feat: Add acknowledgement status and retry for emoji reactions (#4142)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-01-06 11:43:36 -06:00 committed by GitHub
parent 41c5992158
commit 2526728859
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1257 additions and 83 deletions

View file

@ -28,6 +28,7 @@ import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.ReactionEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Position
import org.meshtastic.core.prefs.mesh.MeshPrefs
import org.meshtastic.core.service.MeshServiceNotifications
@ -112,11 +113,11 @@ constructor(
bytes = action.emoji.encodeToByteArray(),
channel = channel,
replyId = action.replyId,
wantAck = false,
wantAck = true,
emoji = action.emoji.codePointAt(0),
)
commandSender.sendData(dataPacket)
rememberReaction(action)
rememberReaction(action, dataPacket.id)
}
private fun handleImportContact(action: ServiceAction.ImportContact, myNodeNum: Int) {
@ -125,7 +126,7 @@ constructor(
nodeManager.handleReceivedUser(verifiedContact.nodeNum, verifiedContact.user, manuallyVerified = true)
}
private fun rememberReaction(action: ServiceAction.Reaction) {
private fun rememberReaction(action: ServiceAction.Reaction, packetId: Int) {
scope.handledLaunch {
val reaction =
ReactionEntity(
@ -136,6 +137,8 @@ constructor(
snr = 0f,
rssi = 0,
hopsAway = 0,
packetId = packetId,
status = MessageStatus.QUEUED,
)
packetRepository.get().insertReaction(reaction)
}

View file

@ -333,10 +333,13 @@ constructor(
packetHandler.removeResponse(packet.decoded.requestId, complete = true)
}
@Suppress("CyclomaticComplexMethod", "LongMethod")
private fun handleAckNak(requestId: Int, fromId: String, routingError: Int, relayNode: Int?) {
scope.handledLaunch {
val isAck = routingError == MeshProtos.Routing.Error.NONE_VALUE
val p = packetRepository.get().getPacketById(requestId)
val reaction = packetRepository.get().getReactionByPacketId(requestId)
val isMaxRetransmit = routingError == MeshProtos.Routing.Error.MAX_RETRANSMIT_VALUE
val shouldRetry =
isMaxRetransmit &&
@ -345,14 +348,22 @@ constructor(
p.data.from == DataPacket.ID_LOCAL &&
p.data.retryCount < MAX_RETRY_ATTEMPTS
val shouldRetryReaction =
isMaxRetransmit &&
reaction != null &&
reaction.userId == DataPacket.ID_LOCAL &&
reaction.retryCount < MAX_RETRY_ATTEMPTS &&
reaction.to != null
@Suppress("MaxLineLength")
Logger.d {
val retryInfo = "packetId=${p?.packetId} dataId=${p?.data?.id} retry=${p?.data?.retryCount}"
val statusInfo = "status=${p?.data?.status}"
val retryInfo =
"packetId=${p?.packetId ?: reaction?.packetId} dataId=${p?.data?.id} retry=${p?.data?.retryCount ?: reaction?.retryCount}"
val statusInfo = "status=${p?.data?.status ?: reaction?.status}"
"[ackNak] req=$requestId routeErr=$routingError isAck=$isAck " +
"maxRetransmit=$isMaxRetransmit shouldRetry=$shouldRetry $retryInfo $statusInfo"
"maxRetransmit=$isMaxRetransmit shouldRetry=$shouldRetry reaction=$shouldRetryReaction $retryInfo $statusInfo"
}
if (shouldRetry && p != null) {
if (shouldRetry) {
val newRetryCount = p.data.retryCount + 1
val newId = commandSender.generatePacketId()
val updatedData =
@ -368,21 +379,66 @@ constructor(
return@handledLaunch
}
if (shouldRetryReaction && reaction != null) {
val newRetryCount = reaction.retryCount + 1
val newId = commandSender.generatePacketId()
val reactionPacket =
DataPacket(
to = reaction.to,
channel = reaction.channel,
bytes = reaction.emoji.toByteArray(Charsets.UTF_8),
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
replyId = reaction.replyId,
wantAck = true,
emoji = reaction.emoji.codePointAt(0),
id = newId,
retryCount = newRetryCount,
)
val updatedReaction =
reaction.copy(
packetId = newId,
status = MessageStatus.QUEUED,
retryCount = newRetryCount,
relayNode = null,
routingError = MeshProtos.Routing.Error.NONE_VALUE,
)
packetRepository.get().updateReaction(updatedReaction)
Logger.w { "[ackNak] retrying reaction req=$requestId newId=$newId retry=$newRetryCount" }
delay(RETRY_DELAY_MS)
commandSender.sendData(reactionPacket)
return@handledLaunch
}
val m =
when {
isAck && fromId == p?.data?.to -> MessageStatus.RECEIVED
isAck && (fromId == p?.data?.to || fromId == reaction?.to) -> MessageStatus.RECEIVED
isAck -> MessageStatus.DELIVERED
else -> MessageStatus.ERROR
}
if (p != null && p.data.status != MessageStatus.RECEIVED) {
p.data.status = m
p.routingError = routingError
p.data.relayNode = relayNode
if (isAck) {
p.data.relays += 1
}
p.data.relayNode = relayNode
packetRepository.get().update(p)
}
reaction?.let { r ->
if (r.status != MessageStatus.RECEIVED) {
var updated = r.copy(status = m, routingError = routingError, relayNode = relayNode)
if (isAck) {
updated = updated.copy(relays = updated.relays + 1)
}
packetRepository.get().updateReaction(updated)
}
}
serviceBroadcasts.broadcastMessageStatus(requestId, m)
}
}

View file

@ -24,6 +24,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.ReactionEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.service.MeshServiceNotifications
import org.meshtastic.proto.Portnums
@ -35,6 +37,8 @@ class ReactionReceiver : BroadcastReceiver() {
@Inject lateinit var meshServiceNotifications: MeshServiceNotifications
@Inject lateinit var packetRepository: PacketRepository
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
companion object {
@ -71,10 +75,24 @@ class ReactionReceiver : BroadcastReceiver() {
bytes = emoji.toByteArray(Charsets.UTF_8),
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
replyId = packetId,
wantAck = true,
emoji = emoji.codePointAt(0),
)
commandSender.sendData(reactionPacket)
val reaction =
ReactionEntity(
replyId = packetId,
userId = DataPacket.ID_LOCAL,
emoji = emoji,
timestamp = System.currentTimeMillis(),
packetId = reactionPacket.id,
status = org.meshtastic.core.model.MessageStatus.QUEUED,
to = toId,
channel = channelIndex,
)
packetRepository.insertReaction(reaction)
// Dismiss the notification after reacting
meshServiceNotifications.cancelMessageNotification(contactKey)
} finally {