mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 08:50:30 +00:00
feat: Enhance MeshCoreBleService with new callbacks and message handling
- Added new callback types for path updates, message sent, message delivered, status responses, binary responses, and battery/storage information. - Implemented handling for binary responses and path updates, including parsing and notifying via callbacks. - Updated message sending logic to include acknowledgment and delivery confirmation. - Enhanced log parsing for received data, including detailed interpretations and analysis. - Introduced status request functionality to query operational status from repeater or sensor nodes. - Updated battery and storage information handling to provide detailed metrics and trigger callbacks. - Deprecated legacy methods in favor of more robust alternatives.
This commit is contained in:
@@ -70,6 +70,30 @@ class AppProvider with ChangeNotifier {
|
||||
connectionProvider.onTelemetryReceived = (publicKey, lppData) {
|
||||
contactsProvider.updateTelemetry(publicKey, lppData);
|
||||
};
|
||||
|
||||
// When a contact's routing path is updated in the mesh network
|
||||
connectionProvider.onPathUpdated = (publicKey) {
|
||||
debugPrint('🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
|
||||
// Trigger a contact sync to get the updated path information
|
||||
// This happens asynchronously to avoid blocking the event handler
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
connectionProvider.getContacts();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// When a message is sent (RESP_CODE_SENT received)
|
||||
connectionProvider.onMessageSent = (messageId, expectedAckTag, suggestedTimeoutMs) {
|
||||
debugPrint('📤 [AppProvider] Message sent - Message ID: $messageId, ACK tag: $expectedAckTag');
|
||||
messagesProvider.markMessageSent(messageId, expectedAckTag, suggestedTimeoutMs);
|
||||
};
|
||||
|
||||
// When a message is delivered (PUSH_CODE_SEND_CONFIRMED received)
|
||||
connectionProvider.onMessageDelivered = (ackCode, roundTripTimeMs) {
|
||||
debugPrint('✅ [AppProvider] Message delivered - ACK: $ackCode, RTT: ${roundTripTimeMs}ms');
|
||||
messagesProvider.markMessageDelivered(ackCode, roundTripTimeMs);
|
||||
};
|
||||
}
|
||||
|
||||
/// Initialize the app (load contacts, sync time, etc.)
|
||||
@@ -89,8 +113,8 @@ class AppProvider with ChangeNotifier {
|
||||
// Automatically login to all saved rooms
|
||||
await _autoLoginToRooms();
|
||||
|
||||
// Sync any waiting messages from device queue
|
||||
await _syncMessages();
|
||||
// Note: Messages are synced automatically via PUSH_CODE_MSG_WAITING events
|
||||
// No need to manually sync here - the BLE service handles this via callbacks
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -195,40 +219,32 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync messages from device queue
|
||||
Future<void> _syncMessages() async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) return;
|
||||
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||
// The ConnectionProvider's onMessageWaiting callback handles automatic message fetching
|
||||
|
||||
try {
|
||||
debugPrint('🔄 [AppProvider] Starting message sync...');
|
||||
final messageCount = await connectionProvider.syncAllMessages();
|
||||
debugPrint('✅ [AppProvider] Synced $messageCount messages');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [AppProvider] Message sync error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh data (contacts, messages)
|
||||
/// Refresh data (contacts only - messages are handled via events)
|
||||
Future<void> refresh() async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) return;
|
||||
|
||||
try {
|
||||
await connectionProvider.getContacts();
|
||||
await _syncMessages();
|
||||
// Messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Refresh error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually sync messages (useful for pull-to-refresh)
|
||||
/// Manually sync messages (only for explicit user pull-to-refresh)
|
||||
/// Note: Messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||
/// This method should ONLY be called when the user explicitly pulls to refresh
|
||||
Future<int> syncMessages() async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) return 0;
|
||||
|
||||
try {
|
||||
debugPrint('🔄 [AppProvider] Manual message sync requested');
|
||||
debugPrint('🔄 [AppProvider] Manual message sync requested (user initiated)');
|
||||
final messageCount = await connectionProvider.syncAllMessages();
|
||||
debugPrint('✅ [AppProvider] Synced $messageCount messages');
|
||||
debugPrint('✅ [AppProvider] Manual sync completed: $messageCount messages');
|
||||
notifyListeners();
|
||||
return messageCount;
|
||||
} catch (e) {
|
||||
|
||||
@@ -50,13 +50,22 @@ class ConnectionProvider with ChangeNotifier {
|
||||
final Map<String, RoomLoginState> _roomLoginStates = {};
|
||||
Map<String, RoomLoginState> get roomLoginStates => Map.unmodifiable(_roomLoginStates);
|
||||
|
||||
// Track sent message IDs by ACK tag for delivery confirmation
|
||||
final Map<int, String> _ackTagToMessageId = {};
|
||||
final List<String> _pendingSentMessageIds = []; // Queue of pending message IDs
|
||||
|
||||
// Callbacks for other providers
|
||||
Function(Contact)? onContactReceived;
|
||||
Function(List<Contact>)? onContactsComplete;
|
||||
Function(Message)? onMessageReceived;
|
||||
Function(Uint8List publicKey, Uint8List lppData)? onTelemetryReceived;
|
||||
Function(Uint8List publicKeyPrefix, int tag, Uint8List responseData)? onBinaryResponse;
|
||||
Function(Uint8List publicKey)? onPathUpdated;
|
||||
Function(Uint8List publicKeyPrefix, int permissions, bool isAdmin, int tag)? onLoginSuccess;
|
||||
Function(Uint8List publicKeyPrefix)? onLoginFail;
|
||||
Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)? onMessageSent;
|
||||
Function(int ackCode, int roundTripTimeMs)? onMessageDelivered;
|
||||
Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse;
|
||||
|
||||
ConnectionProvider() {
|
||||
_initializeBleService();
|
||||
@@ -115,14 +124,23 @@ class ConnectionProvider with ChangeNotifier {
|
||||
onTelemetryReceived?.call(publicKey, lppData);
|
||||
};
|
||||
|
||||
_bleService.onBinaryResponse = (publicKeyPrefix, tag, responseData) {
|
||||
print('📥 [Provider] Binary response received');
|
||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
print(' Tag: $tag');
|
||||
print(' Response data: ${responseData.length} bytes');
|
||||
onBinaryResponse?.call(publicKeyPrefix, tag, responseData);
|
||||
};
|
||||
|
||||
_bleService.onNoMoreMessages = () {
|
||||
print('📥 [Provider] Received NoMoreMessages signal');
|
||||
_noMoreMessages = true;
|
||||
};
|
||||
|
||||
_bleService.onMessageWaiting = () {
|
||||
print('📥 [Provider] Received MsgWaiting push - auto-fetching messages');
|
||||
print('📥 [Provider] PUSH_CODE_MSG_WAITING received - auto-fetching messages via event');
|
||||
// Automatically fetch messages when push notification received
|
||||
// This is the CORRECT way to receive messages - room server pushes them
|
||||
syncAllMessages();
|
||||
};
|
||||
|
||||
@@ -169,6 +187,46 @@ class ConnectionProvider with ChangeNotifier {
|
||||
// which will trigger onContactReceived callback and add/update the contact
|
||||
};
|
||||
|
||||
_bleService.onPathUpdated = (publicKey) {
|
||||
print('📥 [Provider] Path updated for contact');
|
||||
print(' Public key: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...');
|
||||
print(' Note: Mesh network discovered a new/better routing path to this contact');
|
||||
// Forward the callback to ContactsProvider to trigger contact sync
|
||||
onPathUpdated?.call(publicKey);
|
||||
};
|
||||
|
||||
_bleService.onMessageSent = (expectedAckTag, suggestedTimeoutMs, isFloodMode) {
|
||||
print('📥 [Provider] Message sent - ACK tag: $expectedAckTag, timeout: ${suggestedTimeoutMs}ms');
|
||||
|
||||
// Pop the first pending message ID from the queue (FIFO)
|
||||
// This assumes messages are sent sequentially and SENT responses arrive in order
|
||||
if (_pendingSentMessageIds.isNotEmpty) {
|
||||
final messageId = _pendingSentMessageIds.removeAt(0);
|
||||
print(' Matched with message ID: $messageId');
|
||||
|
||||
// Store the ACK tag to message ID mapping for delivery confirmation
|
||||
_ackTagToMessageId[expectedAckTag] = messageId;
|
||||
|
||||
// Notify callback with message ID
|
||||
onMessageSent?.call(messageId, expectedAckTag, suggestedTimeoutMs);
|
||||
} else {
|
||||
print('⚠️ [Provider] SENT response received but no pending message IDs');
|
||||
}
|
||||
};
|
||||
|
||||
_bleService.onMessageDelivered = (ackCode, roundTripTimeMs) {
|
||||
print('📥 [Provider] Message delivered - ACK code: $ackCode, RTT: ${roundTripTimeMs}ms');
|
||||
onMessageDelivered?.call(ackCode, roundTripTimeMs);
|
||||
};
|
||||
|
||||
_bleService.onStatusResponse = (publicKeyPrefix, statusData) {
|
||||
print('📥 [Provider] Status response received from node');
|
||||
print(' Public key prefix: ${publicKeyPrefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}');
|
||||
print(' Status data: ${statusData.length} bytes');
|
||||
// Forward the callback to whoever needs it (e.g., ContactsProvider)
|
||||
onStatusResponse?.call(publicKeyPrefix, statusData);
|
||||
};
|
||||
|
||||
_bleService.onDeviceInfoReceived = (deviceInfo) {
|
||||
print('📥 [Provider] Received DeviceInfo:');
|
||||
print(' Firmware Version: ${deviceInfo['firmwareVersion']}');
|
||||
@@ -218,6 +276,30 @@ class ConnectionProvider with ChangeNotifier {
|
||||
};
|
||||
|
||||
// Activity indicators
|
||||
|
||||
_bleService.onBatteryAndStorage = (millivolts, usedKb, totalKb) {
|
||||
print('📥 [Provider] Received BatteryAndStorage:');
|
||||
print(' Battery: ${millivolts}mV (${(millivolts / 1000.0).toStringAsFixed(2)}V)');
|
||||
if (usedKb != null) {
|
||||
print(' Storage Used: ${usedKb}KB');
|
||||
}
|
||||
if (totalKb != null) {
|
||||
print(' Storage Total: ${totalKb}KB');
|
||||
if (totalKb > 0 && usedKb != null) {
|
||||
final usedPercent = (usedKb / totalKb) * 100.0;
|
||||
print(' Storage Usage: ${usedPercent.toStringAsFixed(1)}%');
|
||||
}
|
||||
}
|
||||
|
||||
_deviceInfo = _deviceInfo.copyWith(
|
||||
batteryMilliVolts: millivolts,
|
||||
storageUsedKb: usedKb,
|
||||
storageTotalKb: totalKb,
|
||||
lastUpdate: DateTime.now(),
|
||||
);
|
||||
notifyListeners();
|
||||
print('✅ [Provider] Device info updated with BatteryAndStorage');
|
||||
};
|
||||
_bleService.onRxActivity = () {
|
||||
_rxActivity = true;
|
||||
notifyListeners();
|
||||
@@ -361,31 +443,53 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Send text message to contact
|
||||
Future<void> sendTextMessage({
|
||||
///
|
||||
/// Returns true if the message was successfully sent to the BLE service.
|
||||
/// Note: This doesn't mean the message was delivered over the mesh network,
|
||||
/// only that it was queued on the companion radio.
|
||||
///
|
||||
/// [messageId] - optional message ID to track delivery status
|
||||
Future<bool> sendTextMessage({
|
||||
required Uint8List contactPublicKey,
|
||||
required String text,
|
||||
String? messageId,
|
||||
}) async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Send the message
|
||||
await _bleService.sendTextMessage(
|
||||
contactPublicKey: contactPublicKey,
|
||||
text: text,
|
||||
);
|
||||
|
||||
// If message ID provided, add it to the pending queue
|
||||
// When the SENT response arrives, it will be matched with this message ID
|
||||
// Note: Messages must be sent sequentially for this to work correctly
|
||||
if (messageId != null) {
|
||||
_pendingSentMessageIds.add(messageId);
|
||||
print(' Added message ID to pending queue: $messageId');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = 'Failed to send message: $e';
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Send channel message
|
||||
///
|
||||
/// [messageId] - optional message ID to track delivery status
|
||||
Future<void> sendChannelMessage({
|
||||
required int channelIdx,
|
||||
required String text,
|
||||
String? messageId,
|
||||
}) async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
@@ -398,6 +502,12 @@ class ConnectionProvider with ChangeNotifier {
|
||||
channelIdx: channelIdx,
|
||||
text: text,
|
||||
);
|
||||
|
||||
// If message ID provided, add it to the pending queue
|
||||
if (messageId != null) {
|
||||
_pendingSentMessageIds.add(messageId);
|
||||
print(' Added message ID to pending queue: $messageId');
|
||||
}
|
||||
} catch (e) {
|
||||
_error = 'Failed to send channel message: $e';
|
||||
notifyListeners();
|
||||
@@ -406,6 +516,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
/// Request telemetry from contact
|
||||
/// [zeroHop] - if true, only direct connection (no mesh forwarding)
|
||||
@Deprecated('Use requestBinary() instead for better functionality')
|
||||
Future<void> requestTelemetry(Uint8List contactPublicKey, {bool zeroHop = false}) async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
@@ -421,6 +532,55 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send binary request to contact (modern replacement for requestTelemetry)
|
||||
///
|
||||
/// Supports multiple request types:
|
||||
/// - Telemetry data (use MeshCoreConstants.binaryReqGetTelemetryData)
|
||||
/// - Average/min/max telemetry (use MeshCoreConstants.binaryReqGetAvgMinMax)
|
||||
/// - Access list (use MeshCoreConstants.binaryReqGetAccessList)
|
||||
/// - Neighbors list (use MeshCoreConstants.binaryReqGetNeighbours)
|
||||
///
|
||||
/// Response arrives via onBinaryResponse callback with matching tag.
|
||||
///
|
||||
/// Example - request telemetry:
|
||||
/// ```dart
|
||||
/// connectionProvider.onBinaryResponse = (prefix, tag, data) {
|
||||
/// // Parse telemetry data (Cayenne LPP format)
|
||||
/// final telemetry = CayenneLppParser.parse(data);
|
||||
/// };
|
||||
/// await connectionProvider.requestBinary(
|
||||
/// contactPublicKey: contact.publicKey,
|
||||
/// requestType: MeshCoreConstants.binaryReqGetTelemetryData,
|
||||
/// );
|
||||
/// ```
|
||||
Future<void> requestBinary({
|
||||
required Uint8List contactPublicKey,
|
||||
required int requestType,
|
||||
Uint8List? additionalParams,
|
||||
}) async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Build request data: request type byte + optional params
|
||||
final requestData = Uint8List.fromList([
|
||||
requestType,
|
||||
if (additionalParams != null) ...additionalParams,
|
||||
]);
|
||||
|
||||
await _bleService.sendBinaryRequest(
|
||||
contactPublicKey: contactPublicKey,
|
||||
requestData: requestData,
|
||||
);
|
||||
} catch (e) {
|
||||
_error = 'Failed to send binary request: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get device time from companion radio to detect clock drift
|
||||
Future<void> getDeviceTime() async {
|
||||
if (!_bleService.isConnected) {
|
||||
@@ -595,6 +755,29 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request battery and storage information
|
||||
///
|
||||
/// Queries the companion radio for:
|
||||
/// - Battery voltage in millivolts
|
||||
/// - Used storage in KB (if available)
|
||||
/// - Total storage in KB (if available)
|
||||
///
|
||||
/// Results arrive via onBatteryAndStorage callback and update deviceInfo.
|
||||
Future<void> getBatteryAndStorage() async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _bleService.getBatteryAndStorage();
|
||||
} catch (e) {
|
||||
_error = 'Failed to get battery and storage: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync messages from device queue
|
||||
/// Call this repeatedly until no more messages are available
|
||||
Future<bool> syncNextMessage() async {
|
||||
@@ -633,6 +816,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
// The device will send ContactMsgRecv or ChannelMsgRecv responses
|
||||
// until it sends NoMoreMessages
|
||||
for (int i = 0; i < 100; i++) { // Safety limit
|
||||
// Check flag BEFORE sending (not after)
|
||||
if (_noMoreMessages) {
|
||||
print('✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests');
|
||||
break;
|
||||
@@ -702,6 +886,33 @@ class ConnectionProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request status from repeater or sensor node
|
||||
///
|
||||
/// Sends a status request to query operational status of a node.
|
||||
/// Results will be delivered via onStatusResponse callback.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// connectionProvider.onStatusResponse = (publicKeyPrefix, statusData) {
|
||||
/// print('Status from node: ${utf8.decode(statusData)}');
|
||||
/// };
|
||||
/// await connectionProvider.requestStatus(repeaterContact.publicKey);
|
||||
/// ```
|
||||
Future<void> requestStatus(Uint8List contactPublicKey) async {
|
||||
if (!_bleService.isConnected) {
|
||||
_error = 'Not connected to device';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _bleService.sendStatusRequest(contactPublicKey);
|
||||
} catch (e) {
|
||||
_error = 'Failed to send status request: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear error message
|
||||
void clearError() {
|
||||
_error = null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
@@ -11,6 +12,12 @@ class MessagesProvider with ChangeNotifier {
|
||||
final MessageStorageService _storageService = MessageStorageService();
|
||||
bool _isInitialized = false;
|
||||
|
||||
// Track pending sent messages by expected ACK/TAG
|
||||
final Map<int, Message> _pendingSentMessages = {};
|
||||
|
||||
// Track timeout timers for pending messages
|
||||
final Map<int, Timer> _timeoutTimers = {};
|
||||
|
||||
List<Message> get messages => List.unmodifiable(_messages);
|
||||
|
||||
List<Message> get contactMessages =>
|
||||
@@ -83,6 +90,17 @@ class MessagesProvider with ChangeNotifier {
|
||||
print(' sarMarkerType: ${enhancedMessage.sarMarkerType}');
|
||||
}
|
||||
|
||||
// Check for duplicates before adding
|
||||
// Messages can arrive multiple times due to:
|
||||
// - Mesh network retransmissions
|
||||
// - Multiple paths in the network
|
||||
// - Syncing messages from device queue
|
||||
if (_isDuplicate(enhancedMessage)) {
|
||||
print('⚠️ [MessagesProvider] Duplicate message detected, skipping: ${enhancedMessage.id}');
|
||||
print(' Text: ${enhancedMessage.text.substring(0, enhancedMessage.text.length > 50 ? 50 : enhancedMessage.text.length)}...');
|
||||
return; // Skip duplicate
|
||||
}
|
||||
|
||||
_messages.add(enhancedMessage);
|
||||
|
||||
// If it's a SAR marker message, extract and store the marker
|
||||
@@ -99,12 +117,65 @@ class MessagesProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check if a message is a duplicate
|
||||
///
|
||||
/// Messages are considered duplicates if they have:
|
||||
/// 1. Same sender public key prefix (for contact messages)
|
||||
/// 2. Same channel index (for channel messages)
|
||||
/// 3. Same sender timestamp
|
||||
/// 4. Same text content
|
||||
bool _isDuplicate(Message message) {
|
||||
return _messages.any((existing) {
|
||||
// Check message type matches
|
||||
if (existing.messageType != message.messageType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check sender matches
|
||||
if (message.isContactMessage) {
|
||||
// For contact messages, compare sender public key prefix
|
||||
if (existing.senderKeyShort != message.senderKeyShort) {
|
||||
return false;
|
||||
}
|
||||
} else if (message.isChannelMessage) {
|
||||
// For channel messages, compare channel index
|
||||
if (existing.channelIdx != message.channelIdx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check timestamp matches (sender timestamp is the unique identifier from the sender)
|
||||
if (existing.senderTimestamp != message.senderTimestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check text content matches
|
||||
if (existing.text != message.text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All criteria match - this is a duplicate
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Add multiple messages
|
||||
void addMessages(List<Message> messages) {
|
||||
int addedCount = 0;
|
||||
int duplicateCount = 0;
|
||||
|
||||
for (final message in messages) {
|
||||
// Always enhance message with SAR parser to detect SAR markers
|
||||
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
|
||||
// Check for duplicates
|
||||
if (_isDuplicate(enhancedMessage)) {
|
||||
duplicateCount++;
|
||||
continue; // Skip duplicate
|
||||
}
|
||||
|
||||
_messages.add(enhancedMessage);
|
||||
addedCount++;
|
||||
|
||||
if (enhancedMessage.isSarMarker) {
|
||||
final marker = enhancedMessage.toSarMarker();
|
||||
@@ -114,6 +185,8 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
print('📥 [MessagesProvider] Added $addedCount messages, skipped $duplicateCount duplicates');
|
||||
|
||||
// Persist to storage asynchronously
|
||||
_persistMessages();
|
||||
|
||||
@@ -231,4 +304,158 @@ class MessagesProvider with ChangeNotifier {
|
||||
'object': objectMarkers.length,
|
||||
};
|
||||
}
|
||||
|
||||
/// Add a sent message with initial status
|
||||
void addSentMessage(Message message) {
|
||||
// Always enhance message with SAR parser to detect SAR markers
|
||||
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||
|
||||
// Check for duplicates (shouldn't happen for sent messages, but be safe)
|
||||
if (_isDuplicate(enhancedMessage)) {
|
||||
print('⚠️ [MessagesProvider] Duplicate sent message detected, skipping: ${enhancedMessage.id}');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add message with sending status
|
||||
final sendingMessage = enhancedMessage.copyWith(
|
||||
deliveryStatus: MessageDeliveryStatus.sending,
|
||||
);
|
||||
_messages.add(sendingMessage);
|
||||
|
||||
// If it's a SAR marker message, extract and store the marker
|
||||
if (sendingMessage.isSarMarker) {
|
||||
final marker = sendingMessage.toSarMarker();
|
||||
if (marker != null) {
|
||||
_sarMarkers[marker.id] = marker;
|
||||
}
|
||||
}
|
||||
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Update message status to sent with ACK tag
|
||||
void markMessageSent(String messageId, int expectedAckTag, int suggestedTimeoutMs) {
|
||||
print('📤 [MessagesProvider] markMessageSent called');
|
||||
print(' Message ID: $messageId');
|
||||
print(' Expected ACK tag: $expectedAckTag');
|
||||
print(' Timeout: ${suggestedTimeoutMs}ms');
|
||||
|
||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||
print(' Message index in list: $index');
|
||||
|
||||
if (index != -1) {
|
||||
final message = _messages[index];
|
||||
print(' Current status: ${message.deliveryStatus}');
|
||||
|
||||
final updatedMessage = message.copyWith(
|
||||
deliveryStatus: MessageDeliveryStatus.sent,
|
||||
expectedAckTag: expectedAckTag,
|
||||
suggestedTimeoutMs: suggestedTimeoutMs,
|
||||
);
|
||||
_messages[index] = updatedMessage;
|
||||
|
||||
// Track by ACK tag for matching with delivery confirmation
|
||||
_pendingSentMessages[expectedAckTag] = updatedMessage;
|
||||
print(' Added to pending messages map with ACK: $expectedAckTag');
|
||||
print(' Total pending messages: ${_pendingSentMessages.length}');
|
||||
|
||||
// Start timeout timer
|
||||
_timeoutTimers[expectedAckTag] = Timer(
|
||||
Duration(milliseconds: suggestedTimeoutMs),
|
||||
() {
|
||||
print('⏱️ [MessagesProvider] Timeout for message $messageId (ACK $expectedAckTag)');
|
||||
if (_pendingSentMessages.containsKey(expectedAckTag)) {
|
||||
markMessageFailed(messageId);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
print('⏱️ [MessagesProvider] Started ${suggestedTimeoutMs}ms timeout timer for message $messageId (ACK $expectedAckTag)');
|
||||
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
} else {
|
||||
print('⚠️ [MessagesProvider] Message not found in list: $messageId');
|
||||
}
|
||||
}
|
||||
|
||||
/// Update message status to delivered with RTT
|
||||
void markMessageDelivered(int ackCode, int roundTripTimeMs) {
|
||||
print('🔍 [MessagesProvider] markMessageDelivered called with ACK: $ackCode, RTT: ${roundTripTimeMs}ms');
|
||||
print(' Current pending messages: ${_pendingSentMessages.keys.toList()}');
|
||||
print(' Looking for ACK: $ackCode');
|
||||
|
||||
// Find message by ACK code
|
||||
final message = _pendingSentMessages[ackCode];
|
||||
if (message != null) {
|
||||
print(' ✅ Found message: ${message.id}');
|
||||
final index = _messages.indexWhere((m) => m.id == message.id);
|
||||
print(' Message index in list: $index');
|
||||
|
||||
if (index != -1) {
|
||||
final updatedMessage = message.copyWith(
|
||||
deliveryStatus: MessageDeliveryStatus.delivered,
|
||||
roundTripTimeMs: roundTripTimeMs,
|
||||
deliveredAt: DateTime.now(),
|
||||
);
|
||||
_messages[index] = updatedMessage;
|
||||
|
||||
// Cancel timeout timer
|
||||
_timeoutTimers[ackCode]?.cancel();
|
||||
_timeoutTimers.remove(ackCode);
|
||||
|
||||
// Remove from pending
|
||||
_pendingSentMessages.remove(ackCode);
|
||||
|
||||
print('✅ [MessagesProvider] Message ${message.id} delivered in ${roundTripTimeMs}ms (ACK $ackCode)');
|
||||
print(' Calling notifyListeners() to update UI');
|
||||
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
} else {
|
||||
print('⚠️ [MessagesProvider] Message not found in list (index=-1)');
|
||||
}
|
||||
} else {
|
||||
print('⚠️ [MessagesProvider] No pending message found for ACK code: $ackCode');
|
||||
print(' This means either:');
|
||||
print(' 1. markMessageSent() was never called for this message');
|
||||
print(' 2. The ACK code doesn\'t match the expected ACK tag from RESP_CODE_SENT');
|
||||
print(' 3. The message was already delivered or timed out');
|
||||
}
|
||||
}
|
||||
|
||||
/// Update message status to failed
|
||||
void markMessageFailed(String messageId) {
|
||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||
if (index != -1) {
|
||||
final message = _messages[index];
|
||||
final updatedMessage = message.copyWith(
|
||||
deliveryStatus: MessageDeliveryStatus.failed,
|
||||
);
|
||||
_messages[index] = updatedMessage;
|
||||
|
||||
// Cancel timeout timer if it exists
|
||||
if (message.expectedAckTag != null) {
|
||||
_timeoutTimers[message.expectedAckTag]?.cancel();
|
||||
_timeoutTimers.remove(message.expectedAckTag);
|
||||
_pendingSentMessages.remove(message.expectedAckTag);
|
||||
}
|
||||
|
||||
print('❌ [MessagesProvider] Message $messageId marked as failed');
|
||||
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Cancel all pending timeout timers
|
||||
for (final timer in _timeoutTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_timeoutTimers.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user