mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Add SFPP confirmed status to Messages and Reactions (#4139)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> Co-authored-by: Mac DeCourcy <github.znq26@slmail.me>
This commit is contained in:
parent
78bd1ad6dd
commit
782c068ead
21 changed files with 2699 additions and 61 deletions
|
|
@ -75,7 +75,7 @@ constructor(
|
|||
when (action) {
|
||||
is ServiceAction.Favorite -> handleFavorite(action, myNodeNum)
|
||||
is ServiceAction.Ignore -> handleIgnore(action, myNodeNum)
|
||||
is ServiceAction.Reaction -> handleReaction(action)
|
||||
is ServiceAction.Reaction -> handleReaction(action, myNodeNum)
|
||||
is ServiceAction.ImportContact -> handleImportContact(action, myNodeNum)
|
||||
is ServiceAction.SendContact -> {
|
||||
commandSender.sendAdmin(myNodeNum) { addContact = action.contact }
|
||||
|
|
@ -103,21 +103,23 @@ constructor(
|
|||
nodeManager.updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
|
||||
}
|
||||
|
||||
private fun handleReaction(action: ServiceAction.Reaction) {
|
||||
private fun handleReaction(action: ServiceAction.Reaction, myNodeNum: Int) {
|
||||
val channel = action.contactKey[0].digitToInt()
|
||||
val destId = action.contactKey.substring(1)
|
||||
val dataPacket =
|
||||
org.meshtastic.core.model.DataPacket(
|
||||
to = destId,
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = action.emoji.encodeToByteArray(),
|
||||
channel = channel,
|
||||
replyId = action.replyId,
|
||||
wantAck = true,
|
||||
emoji = action.emoji.codePointAt(0),
|
||||
)
|
||||
org.meshtastic.core.model
|
||||
.DataPacket(
|
||||
to = destId,
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = action.emoji.encodeToByteArray(),
|
||||
channel = channel,
|
||||
replyId = action.replyId,
|
||||
wantAck = true,
|
||||
emoji = action.emoji.codePointAt(0),
|
||||
)
|
||||
.apply { from = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL }
|
||||
commandSender.sendData(dataPacket)
|
||||
rememberReaction(action, dataPacket.id)
|
||||
rememberReaction(action, dataPacket.id, myNodeNum)
|
||||
}
|
||||
|
||||
private fun handleImportContact(action: ServiceAction.ImportContact, myNodeNum: Int) {
|
||||
|
|
@ -126,12 +128,13 @@ constructor(
|
|||
nodeManager.handleReceivedUser(verifiedContact.nodeNum, verifiedContact.user, manuallyVerified = true)
|
||||
}
|
||||
|
||||
private fun rememberReaction(action: ServiceAction.Reaction, packetId: Int) {
|
||||
private fun rememberReaction(action: ServiceAction.Reaction, packetId: Int, myNodeNum: Int) {
|
||||
scope.handledLaunch {
|
||||
val reaction =
|
||||
ReactionEntity(
|
||||
myNodeNum = myNodeNum,
|
||||
replyId = action.replyId,
|
||||
userId = nodeManager.getMyId(),
|
||||
userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL,
|
||||
emoji = action.emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
snr = 0f,
|
||||
|
|
@ -139,6 +142,8 @@ constructor(
|
|||
hopsAway = 0,
|
||||
packetId = packetId,
|
||||
status = MessageStatus.QUEUED,
|
||||
to = action.contactKey.substring(1),
|
||||
channel = action.contactKey[0].digitToInt(),
|
||||
)
|
||||
packetRepository.get().insertReaction(reaction)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import org.meshtastic.core.database.entity.Packet
|
|||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.SfppHasher
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
|
|
@ -58,7 +59,7 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
|
||||
@Singleton
|
||||
class MeshDataHandler
|
||||
@Inject
|
||||
|
|
@ -191,9 +192,82 @@ constructor(
|
|||
handleReceivedStoreAndForward(dataPacket, u, myNodeNum)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleStoreForwardPlusPlus(packet: MeshPacket) {
|
||||
val sfpp = MeshProtos.StoreForwardPlusPlus.parseFrom(packet.decoded.payload)
|
||||
Logger.d { "Received StoreForwardPlusPlus packet: $sfpp" }
|
||||
|
||||
when (sfpp.sfppMessageType) {
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE,
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_FIRSTHALF,
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_SECONDHALF,
|
||||
-> {
|
||||
val isFragment = sfpp.sfppMessageType != MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE
|
||||
|
||||
// If it has a commit hash, it's already on the chain (Confirmed)
|
||||
// Otherwise it's still being routed via SF++ (Routing)
|
||||
val status = if (sfpp.commitHash.isEmpty) MessageStatus.SFPP_ROUTING else MessageStatus.SFPP_CONFIRMED
|
||||
|
||||
// Prefer a full 16-byte hash calculated from the message bytes if available
|
||||
// But only if it's NOT a fragment, otherwise the calculated hash would be wrong
|
||||
val hash =
|
||||
when {
|
||||
!sfpp.messageHash.isEmpty -> sfpp.messageHash.toByteArray()
|
||||
!isFragment && !sfpp.message.isEmpty -> {
|
||||
SfppHasher.computeMessageHash(
|
||||
encryptedPayload = sfpp.message.toByteArray(),
|
||||
// Map 0 back to NODENUM_BROADCAST to match firmware hash calculation
|
||||
to =
|
||||
if (sfpp.encapsulatedTo == 0) DataPacket.NODENUM_BROADCAST else sfpp.encapsulatedTo,
|
||||
from = sfpp.encapsulatedFrom,
|
||||
id = sfpp.encapsulatedId,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
} ?: return
|
||||
|
||||
Logger.d {
|
||||
"SFPP updateStatus: packetId=${sfpp.encapsulatedId} from=${sfpp.encapsulatedFrom} " +
|
||||
"to=${sfpp.encapsulatedTo} myNodeNum=${nodeManager.myNodeNum} status=$status"
|
||||
}
|
||||
scope.handledLaunch {
|
||||
packetRepository
|
||||
.get()
|
||||
.updateSFPPStatus(
|
||||
packetId = sfpp.encapsulatedId,
|
||||
from = sfpp.encapsulatedFrom,
|
||||
to = sfpp.encapsulatedTo,
|
||||
hash = hash,
|
||||
status = status,
|
||||
rxTime = sfpp.encapsulatedRxtime.toLong() and 0xFFFFFFFFL,
|
||||
myNodeNum = nodeManager.myNodeNum,
|
||||
)
|
||||
serviceBroadcasts.broadcastMessageStatus(sfpp.encapsulatedId, status)
|
||||
}
|
||||
}
|
||||
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.CANON_ANNOUNCE -> {
|
||||
scope.handledLaunch {
|
||||
packetRepository
|
||||
.get()
|
||||
.updateSFPPStatusByHash(
|
||||
hash = sfpp.messageHash.toByteArray(),
|
||||
status = MessageStatus.SFPP_CONFIRMED,
|
||||
rxTime = sfpp.encapsulatedRxtime.toLong() and 0xFFFFFFFFL,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.CHAIN_QUERY -> {
|
||||
Logger.i { "SF++: Node ${packet.from} is querying chain status" }
|
||||
}
|
||||
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_REQUEST -> {
|
||||
Logger.i { "SF++: Node ${packet.from} is requesting links" }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePaxCounter(packet: MeshPacket) {
|
||||
|
|
@ -345,13 +419,13 @@ constructor(
|
|||
isMaxRetransmit &&
|
||||
p != null &&
|
||||
p.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE &&
|
||||
p.data.from == DataPacket.ID_LOCAL &&
|
||||
(p.data.from == DataPacket.ID_LOCAL || p.data.from == nodeManager.getMyId()) &&
|
||||
p.data.retryCount < MAX_RETRY_ATTEMPTS
|
||||
|
||||
val shouldRetryReaction =
|
||||
isMaxRetransmit &&
|
||||
reaction != null &&
|
||||
reaction.userId == DataPacket.ID_LOCAL &&
|
||||
(reaction.userId == DataPacket.ID_LOCAL || reaction.userId == nodeManager.getMyId()) &&
|
||||
reaction.retryCount < MAX_RETRY_ATTEMPTS &&
|
||||
reaction.to != null
|
||||
@Suppress("MaxLineLength")
|
||||
|
|
@ -509,7 +583,8 @@ constructor(
|
|||
|
||||
fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean = true) {
|
||||
if (dataPacket.dataType !in rememberDataType) return
|
||||
val fromLocal = dataPacket.from == DataPacket.ID_LOCAL
|
||||
val fromLocal =
|
||||
dataPacket.from == DataPacket.ID_LOCAL || dataPacket.from == DataPacket.nodeNumToDefaultId(myNodeNum)
|
||||
val toBroadcast = dataPacket.to == DataPacket.ID_BROADCAST
|
||||
val contactId = if (fromLocal || toBroadcast) dataPacket.to else dataPacket.from
|
||||
|
||||
|
|
@ -529,7 +604,6 @@ constructor(
|
|||
snr = dataPacket.snr,
|
||||
rssi = dataPacket.rssi,
|
||||
hopsAway = dataPacket.hopsAway,
|
||||
replyId = dataPacket.replyId ?: 0,
|
||||
)
|
||||
scope.handledLaunch {
|
||||
packetRepository.get().apply {
|
||||
|
|
@ -593,10 +667,14 @@ constructor(
|
|||
|
||||
private fun rememberReaction(packet: MeshPacket) = scope.handledLaunch {
|
||||
val emoji = packet.decoded.payload.toByteArray().decodeToString()
|
||||
val fromId = dataMapper.toNodeID(packet.from)
|
||||
val toId = dataMapper.toNodeID(packet.to)
|
||||
|
||||
val reaction =
|
||||
ReactionEntity(
|
||||
myNodeNum = nodeManager.myNodeNum ?: 0,
|
||||
replyId = packet.decoded.replyId,
|
||||
userId = dataMapper.toNodeID(packet.from),
|
||||
userId = fromId,
|
||||
emoji = emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
snr = packet.rxSnr,
|
||||
|
|
@ -607,6 +685,10 @@ constructor(
|
|||
} else {
|
||||
packet.hopStart - packet.hopLimit
|
||||
},
|
||||
packetId = packet.id,
|
||||
status = MessageStatus.RECEIVED,
|
||||
to = toId,
|
||||
channel = packet.channel,
|
||||
)
|
||||
packetRepository.get().insertReaction(reaction)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ class ReactionReceiver : BroadcastReceiver() {
|
|||
|
||||
@Inject lateinit var packetRepository: PacketRepository
|
||||
|
||||
@Inject lateinit var nodeManager: MeshNodeManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
companion object {
|
||||
|
|
@ -82,8 +84,9 @@ class ReactionReceiver : BroadcastReceiver() {
|
|||
|
||||
val reaction =
|
||||
ReactionEntity(
|
||||
myNodeNum = nodeManager.myNodeNum ?: 0,
|
||||
replyId = packetId,
|
||||
userId = DataPacket.ID_LOCAL,
|
||||
userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL,
|
||||
emoji = emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
packetId = reactionPacket.id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue