feat: Implement automatic reconnection handling and enhance message processing with sender identification

This commit is contained in:
Janez T
2025-10-15 14:53:55 +02:00
parent f50b429e34
commit 829b8605eb
9 changed files with 371 additions and 54 deletions

View File

@@ -53,7 +53,24 @@ class AppProvider with ChangeNotifier {
// When a message is received
connectionProvider.onMessageReceived = (message) {
messagesProvider.addMessage(message);
// Pass contact lookup function to link channel messages with contacts
messagesProvider.addMessage(
message,
contactLookup: (name) {
// Find contact by name and return their public key hex (first 12 chars for 6 bytes)
try {
final contact = contactsProvider.contacts.firstWhere(
(c) => c.advName == name,
);
return contact.publicKeyHex.isNotEmpty && contact.publicKeyHex.length >= 12
? contact.publicKeyHex.substring(0, 12)
: '';
} catch (e) {
// No matching contact found
return '';
}
},
);
// Optionally update sender name from contacts
if (message.senderPublicKeyPrefix != null) {

View File

@@ -42,6 +42,11 @@ class ConnectionProvider with ChangeNotifier {
int get rxPacketCount => _bleService.rxPacketCount;
int get txPacketCount => _bleService.txPacketCount;
// Reconnection state (exposed from BLE service)
bool get isReconnecting => _bleService.isReconnecting;
int get reconnectionAttempt => _bleService.reconnectionAttempt;
int get maxReconnectionAttempts => _bleService.maxReconnectionAttempts;
// Message sync state
bool _noMoreMessages = false;
@@ -75,15 +80,24 @@ class ConnectionProvider with ChangeNotifier {
_deviceInfo = _deviceInfo.copyWith(
connectionState: isConnected
? ConnectionState.connected
: ConnectionState.disconnected,
: (_bleService.isReconnecting
? ConnectionState.connecting
: ConnectionState.disconnected),
lastUpdate: DateTime.now(),
);
print(' Updated deviceInfo.connectionState: ${_deviceInfo.connectionState}');
print(' Updated deviceInfo.isConnected: ${_deviceInfo.isConnected}');
print(' isReconnecting: ${_bleService.isReconnecting}');
notifyListeners();
print(' Notified listeners');
};
_bleService.onReconnectionAttempt = (attemptNumber, maxAttempts) {
print('🔄 [Provider] Reconnection attempt $attemptNumber/$maxAttempts');
// Notify UI to update reconnection status display
notifyListeners();
};
_bleService.onError = (error) {
print('⚠️ [Provider] BLE error received: $error');
print(' Current connection state: ${_deviceInfo.connectionState}');
@@ -400,6 +414,13 @@ class ConnectionProvider with ChangeNotifier {
notifyListeners();
}
/// Cancel ongoing reconnection attempts
/// This is useful when the user wants to manually disconnect during reconnection
void cancelReconnection() {
print('🔴 [Provider] User requested cancellation of reconnection');
disconnect();
}
/// Get contacts from device
Future<void> getContacts() async {
if (!_bleService.isConnected) {
@@ -479,6 +500,8 @@ class ConnectionProvider with ChangeNotifier {
/// Send channel message
///
/// [messageId] - optional message ID to track delivery status
/// Note: Channel messages are ephemeral (not persisted), so they're marked
/// as "sent" immediately upon receiving OK response from the device.
Future<void> sendChannelMessage({
required int channelIdx,
required String text,
@@ -491,16 +514,19 @@ class ConnectionProvider with ChangeNotifier {
}
try {
// IMPORTANT: Track pending message BEFORE sending to avoid race condition
if (messageId != null) {
_messageDeliveryTracker.trackPendingMessage(messageId);
print(' Added message ID to pending queue BEFORE sending: $messageId');
}
await _bleService.sendChannelMessage(
channelIdx: channelIdx,
text: text,
);
// Channel messages are ephemeral (not persisted) - mark as "sent" immediately
// They don't have ACK/TAG mechanism like direct messages
if (messageId != null) {
print('✅ [Provider] Channel message sent successfully - marking as sent: $messageId');
// Use a dummy ACK tag (0) and timeout (0) for channel messages
// This will trigger the callback to mark the message as "sent"
onMessageSent?.call(messageId, 0, 0);
}
} catch (e) {
_error = 'Failed to send channel message: $e';
notifyListeners();

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import '../models/message.dart';
import '../models/sar_marker.dart';
@@ -79,15 +80,41 @@ class MessagesProvider with ChangeNotifier {
}
/// Add a message
void addMessage(Message message) {
/// If [contactLookup] function is provided, it will be used to match channel
/// message senders with known contacts by name
void addMessage(Message message, {String Function(String name)? contactLookup}) {
// Always enhance message with SAR parser to detect SAR markers
final enhancedMessage = SarMessageParser.enhanceMessage(message);
// For channel messages with sender name, try to link with contact
Message finalMessage = enhancedMessage;
if (enhancedMessage.isChannelMessage &&
enhancedMessage.senderName != null &&
contactLookup != null) {
// Look up contact public key by name
final publicKeyHex = contactLookup(enhancedMessage.senderName!);
if (publicKeyHex.isNotEmpty) {
// Convert hex string to bytes (first 6 bytes)
final publicKeyBytes = <int>[];
for (int i = 0; i < 12 && i < publicKeyHex.length; i += 2) {
final byteString = publicKeyHex.substring(i, i + 2);
publicKeyBytes.add(int.parse(byteString, radix: 16));
}
if (publicKeyBytes.length == 6) {
// Add public key prefix to message
finalMessage = enhancedMessage.copyWith(
senderPublicKeyPrefix: Uint8List.fromList(publicKeyBytes),
);
}
}
}
// Debug: Check if message is SAR
if (message.text.startsWith('S:')) {
print('🔍 [MessagesProvider] Processing SAR message: ${message.text}');
print(' isSarMarker: ${enhancedMessage.isSarMarker}');
print(' sarMarkerType: ${enhancedMessage.sarMarkerType}');
print(' isSarMarker: ${finalMessage.isSarMarker}');
print(' sarMarkerType: ${finalMessage.sarMarkerType}');
}
// Check for duplicates before adding
@@ -95,17 +122,17 @@ class MessagesProvider with ChangeNotifier {
// - 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)}...');
if (_isDuplicate(finalMessage)) {
print('⚠️ [MessagesProvider] Duplicate message detected, skipping: ${finalMessage.id}');
print(' Text: ${finalMessage.text.substring(0, finalMessage.text.length > 50 ? 50 : finalMessage.text.length)}...');
return; // Skip duplicate
}
_messages.add(enhancedMessage);
_messages.add(finalMessage);
// If it's a SAR marker message, extract and store the marker
if (enhancedMessage.isSarMarker) {
final marker = enhancedMessage.toSarMarker();
if (finalMessage.isSarMarker) {
final marker = finalMessage.toSarMarker();
if (marker != null) {
_sarMarkers[marker.id] = marker;
}
@@ -348,33 +375,40 @@ class MessagesProvider with ChangeNotifier {
if (index != -1) {
final message = _messages[index];
print(' Current status: ${message.deliveryStatus}');
print(' Message type: ${message.messageType}');
print(' Message text preview: ${message.text.substring(0, message.text.length > 30 ? 30 : message.text.length)}...');
final updatedMessage = message.copyWith(
deliveryStatus: MessageDeliveryStatus.sent,
expectedAckTag: expectedAckTag,
suggestedTimeoutMs: suggestedTimeoutMs,
expectedAckTag: expectedAckTag > 0 ? expectedAckTag : null,
suggestedTimeoutMs: suggestedTimeoutMs > 0 ? suggestedTimeoutMs : null,
);
_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}');
print(' Pending ACKs after adding: ${_pendingSentMessages.keys.toList()}');
// Only track and set timeout for direct messages (channel messages have expectedAckTag=0)
if (expectedAckTag > 0 && suggestedTimeoutMs > 0) {
// 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}');
print(' Pending ACKs after adding: ${_pendingSentMessages.keys.toList()}');
// Start timeout timer
_timeoutTimers[expectedAckTag] = Timer(
Duration(milliseconds: suggestedTimeoutMs),
() {
print('⏱️ [MessagesProvider] Timeout for message $messageId (ACK $expectedAckTag)');
if (_pendingSentMessages.containsKey(expectedAckTag)) {
markMessageFailed(messageId);
}
},
);
// 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)');
} else {
print(' Channel message (no ACK tracking) - marked as sent immediately');
}
print('⏱️ [MessagesProvider] Started ${suggestedTimeoutMs}ms timeout timer for message $messageId (ACK $expectedAckTag)');
print(' Calling notifyListeners() to update UI with "sent" status');
_persistMessages();