mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-18 08:26:27 +10:00
Mark pending channel messages sent on RESP_CODE_SENT (#186)
* Mark pending channel message sent on RESP_CODE_SENT * Disambiguate RESP_CODE_SENT handling for direct vs channel * Handle channel sent feedback when firmware returns RESP_CODE_OK * Correlate channel OK ACKs and queue reaction channel sends
This commit is contained in:
@@ -114,6 +114,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final List<Channel> _channels = [];
|
||||
final Map<String, List<Message>> _conversations = {};
|
||||
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
||||
final List<String> _pendingChannelSentQueue = [];
|
||||
final List<_PendingCommandAck> _pendingGenericAckQueue = [];
|
||||
static const String _reactionSendQueuePrefix = '__reaction_send__';
|
||||
int _reactionSendQueueSequence = 0;
|
||||
final Set<String> _loadedConversationKeys = {};
|
||||
final Map<int, Set<String>> _processedChannelReactions =
|
||||
{}; // channelIndex -> Set of "targetHash_emoji"
|
||||
@@ -988,6 +992,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_hasLoadedChannels = false;
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
if (!manual) {
|
||||
@@ -995,7 +1002,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendFrame(Uint8List data) async {
|
||||
Future<void> sendFrame(
|
||||
Uint8List data, {
|
||||
String? channelSendQueueId,
|
||||
bool expectsGenericAck = false,
|
||||
}) async {
|
||||
if (!isConnected || _rxCharacteristic == null) {
|
||||
throw Exception("Not connected to a MeshCore device");
|
||||
}
|
||||
@@ -1014,6 +1025,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
data.toList(),
|
||||
withoutResponse: canWriteWithoutResponse,
|
||||
);
|
||||
_trackPendingGenericAck(
|
||||
data,
|
||||
channelSendQueueId: channelSendQueueId,
|
||||
expectsGenericAck: expectsGenericAck,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> requestBatteryStatus({bool force = false}) async {
|
||||
@@ -1369,7 +1385,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
// Send the reaction to the device (don't add as a visible message)
|
||||
await sendFrame(buildSendChannelTextMsgFrame(channel.index, text));
|
||||
final reactionQueueId = _nextReactionSendQueueId();
|
||||
_pendingChannelSentQueue.add(reactionQueueId);
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, text),
|
||||
channelSendQueueId: reactionQueueId,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1379,6 +1401,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
channel.index,
|
||||
);
|
||||
_addChannelMessage(channel.index, message);
|
||||
_pendingChannelSentQueue.add(message.messageId);
|
||||
notifyListeners();
|
||||
|
||||
final trimmed = text.trim();
|
||||
@@ -1388,7 +1411,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||
? Smaz.encodeIfSmaller(text)
|
||||
: text;
|
||||
await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText));
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||
channelSendQueueId: message.messageId,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeContact(Contact contact) async {
|
||||
@@ -1735,6 +1762,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
debugPrint('RX frame: code=$code len=${frame.length}');
|
||||
|
||||
switch (code) {
|
||||
case respCodeOk:
|
||||
_handleOk();
|
||||
break;
|
||||
case respCodeDeviceInfo:
|
||||
_handleDeviceInfo(frame);
|
||||
break;
|
||||
@@ -1829,6 +1859,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
'Firmware responded with error code: $errCode',
|
||||
tag: 'Protocol',
|
||||
);
|
||||
|
||||
if (_pendingGenericAckQueue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final failedAck = _pendingGenericAckQueue.removeAt(0);
|
||||
if (failedAck.commandCode != cmdSendChannelTxtMsg ||
|
||||
failedAck.channelSendQueueId == null) {
|
||||
return;
|
||||
}
|
||||
_pendingChannelSentQueue.remove(failedAck.channelSendQueueId);
|
||||
}
|
||||
|
||||
void _handlePathUpdated(Uint8List frame) {
|
||||
@@ -2611,8 +2652,22 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_retryService != null) {
|
||||
_retryService!.updateMessageFromSent(ackHash, timeoutMs);
|
||||
final retryService = _retryService;
|
||||
if (retryService != null &&
|
||||
retryService.updateMessageFromSent(
|
||||
ackHash,
|
||||
timeoutMs,
|
||||
allowQueueFallback: false,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_markNextPendingChannelMessageSent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (retryService != null) {
|
||||
retryService.updateMessageFromSent(ackHash, timeoutMs);
|
||||
}
|
||||
} else {
|
||||
// Fallback to old behavior
|
||||
@@ -2629,6 +2684,64 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool _markNextPendingChannelMessageSent() {
|
||||
while (_pendingChannelSentQueue.isNotEmpty) {
|
||||
final queuedMessageId = _pendingChannelSentQueue.removeAt(0);
|
||||
if (_isReactionSendQueueId(queuedMessageId)) {
|
||||
return true;
|
||||
}
|
||||
if (_markPendingChannelMessageSentById(queuedMessageId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _markPendingChannelMessageSentById(String messageId) {
|
||||
for (final entry in _channelMessages.entries) {
|
||||
final channelMessages = entry.value;
|
||||
for (int i = channelMessages.length - 1; i >= 0; i--) {
|
||||
final message = channelMessages[i];
|
||||
if (message.messageId != messageId) {
|
||||
continue;
|
||||
}
|
||||
if (!message.isOutgoing ||
|
||||
message.status != ChannelMessageStatus.pending) {
|
||||
return false;
|
||||
}
|
||||
channelMessages[i] = message.copyWith(
|
||||
status: ChannelMessageStatus.sent,
|
||||
);
|
||||
_pendingChannelSentQueue.remove(messageId);
|
||||
unawaited(
|
||||
_channelMessageStore.saveChannelMessages(entry.key, channelMessages),
|
||||
);
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _handleOk() {
|
||||
if (_pendingGenericAckQueue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pendingAck = _pendingGenericAckQueue.removeAt(0);
|
||||
if (pendingAck.commandCode != cmdSendChannelTxtMsg ||
|
||||
pendingAck.channelSendQueueId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final queueId = pendingAck.channelSendQueueId!;
|
||||
_pendingChannelSentQueue.remove(queueId);
|
||||
if (_isReactionSendQueueId(queueId)) {
|
||||
return;
|
||||
}
|
||||
_markPendingChannelMessageSentById(queueId);
|
||||
}
|
||||
|
||||
void _handleSendConfirmed(Uint8List frame) {
|
||||
// Frame format from C++:
|
||||
// [0] = PUSH_CODE_SEND_CONFIRMED
|
||||
@@ -3207,18 +3320,22 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
mergedPathBytes.length,
|
||||
);
|
||||
final newRepeatCount = existing.repeatCount + 1;
|
||||
final promotedFromPending =
|
||||
newRepeatCount == 1 &&
|
||||
existing.status == ChannelMessageStatus.pending;
|
||||
messages[existingIndex] = existing.copyWith(
|
||||
repeatCount: newRepeatCount,
|
||||
pathLength: mergedPathLength,
|
||||
pathBytes: mergedPathBytes,
|
||||
pathVariants: mergedPathVariants,
|
||||
// Mark as sent when first repeat is heard
|
||||
status:
|
||||
newRepeatCount == 1 &&
|
||||
existing.status == ChannelMessageStatus.pending
|
||||
status: promotedFromPending
|
||||
? ChannelMessageStatus.sent
|
||||
: existing.status,
|
||||
);
|
||||
if (promotedFromPending) {
|
||||
_pendingChannelSentQueue.remove(existing.messageId);
|
||||
}
|
||||
} else {
|
||||
messages.add(processedMessage);
|
||||
}
|
||||
@@ -3391,11 +3508,37 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_queuedMessageSyncInFlight = false;
|
||||
_isSyncingChannels = false;
|
||||
_channelSyncInFlight = false;
|
||||
_pendingChannelSentQueue.clear();
|
||||
_pendingGenericAckQueue.clear();
|
||||
_reactionSendQueueSequence = 0;
|
||||
|
||||
_setState(MeshCoreConnectionState.disconnected);
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
void _trackPendingGenericAck(
|
||||
Uint8List data, {
|
||||
String? channelSendQueueId,
|
||||
required bool expectsGenericAck,
|
||||
}) {
|
||||
if (!expectsGenericAck || data.isEmpty) return;
|
||||
_pendingGenericAckQueue.add(
|
||||
_PendingCommandAck(
|
||||
commandCode: data[0],
|
||||
channelSendQueueId: channelSendQueueId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _nextReactionSendQueueId() {
|
||||
_reactionSendQueueSequence++;
|
||||
return '$_reactionSendQueuePrefix$_reactionSendQueueSequence';
|
||||
}
|
||||
|
||||
bool _isReactionSendQueueId(String queueId) {
|
||||
return queueId.startsWith(_reactionSendQueuePrefix);
|
||||
}
|
||||
|
||||
Map<String, String> _parseKeyValueString(String input) {
|
||||
final result = <String, String>{};
|
||||
|
||||
@@ -3691,3 +3834,10 @@ class _RepeaterAckContext {
|
||||
required this.messageBytes,
|
||||
});
|
||||
}
|
||||
|
||||
class _PendingCommandAck {
|
||||
final int commandCode;
|
||||
final String? channelSendQueueId;
|
||||
|
||||
_PendingCommandAck({required this.commandCode, this.channelSendQueueId});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user