mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Implement automatic reconnection handling and enhance message processing with sender identification
This commit is contained in:
@@ -165,6 +165,24 @@ class Message {
|
||||
/// Check if this is a sent message (not received)
|
||||
bool get isSentMessage => deliveryStatus != MessageDeliveryStatus.received;
|
||||
|
||||
/// Check if this message is from self (own message)
|
||||
/// [selfPublicKey] - the device's own public key (first 6 bytes)
|
||||
bool isFromSelf(Uint8List? selfPublicKey) {
|
||||
if (selfPublicKey == null || selfPublicKey.length < 6) return false;
|
||||
|
||||
// Compare sender public key prefix with self public key prefix
|
||||
if (senderPublicKeyPrefix != null && senderPublicKeyPrefix!.length >= 6) {
|
||||
return senderPublicKeyPrefix![0] == selfPublicKey[0] &&
|
||||
senderPublicKeyPrefix![1] == selfPublicKey[1] &&
|
||||
senderPublicKeyPrefix![2] == selfPublicKey[2] &&
|
||||
senderPublicKeyPrefix![3] == selfPublicKey[3] &&
|
||||
senderPublicKeyPrefix![4] == selfPublicKey[4] &&
|
||||
senderPublicKeyPrefix![5] == selfPublicKey[5];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Message copyWith({
|
||||
String? id,
|
||||
MessageType? messageType,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -335,28 +335,64 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
Text(
|
||||
isConnected
|
||||
? deviceInfo.displayName ?? 'Connected'
|
||||
: 'Disconnected',
|
||||
: (provider.isReconnecting
|
||||
? 'Reconnecting... (${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts})'
|
||||
: 'Disconnected'),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
color: provider.isReconnecting
|
||||
? Colors.orange[600]
|
||||
: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isConnected)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showConnectionDialog(context),
|
||||
icon: const Icon(Icons.bluetooth, size: 18),
|
||||
label: const Text('Connect'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: provider.isReconnecting
|
||||
? null // Disable button during reconnection
|
||||
: () => _showConnectionDialog(context),
|
||||
icon: provider.isReconnecting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.black54),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth, size: 18),
|
||||
label: Text(provider.isReconnecting
|
||||
? 'Reconnecting (${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts})'
|
||||
: 'Connect'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Cancel button during reconnection
|
||||
if (provider.isReconnecting) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => provider.cancelReconnection(),
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
tooltip: 'Cancel reconnection',
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
|
||||
@@ -503,6 +503,11 @@ class _MessageBubble extends StatelessWidget {
|
||||
final isSarMarker = message.isSarMarker;
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// Get device's own public key from ConnectionProvider
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final selfPublicKey = connectionProvider.deviceInfo.publicKey;
|
||||
final isOwnMessage = message.isFromSelf(selfPublicKey);
|
||||
|
||||
// Debug: Log message details
|
||||
if (message.text.startsWith('S:')) {
|
||||
debugPrint('🎨 [MessageBubble] Rendering SAR message:');
|
||||
@@ -519,14 +524,19 @@ class _MessageBubble extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: isSarMarker
|
||||
? _getSarMarkerColor(context, isDarkMode)
|
||||
: Theme.of(context).colorScheme.surfaceVariant,
|
||||
: _getMessageBubbleColor(context, isOwnMessage, isDarkMode),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: isSarMarker
|
||||
? Border.all(
|
||||
color: _getSarMarkerBorderColor(context, isDarkMode),
|
||||
width: 3,
|
||||
)
|
||||
: null,
|
||||
: isOwnMessage
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
boxShadow: isSarMarker
|
||||
? [
|
||||
BoxShadow(
|
||||
@@ -574,15 +584,18 @@ class _MessageBubble extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
if (message.isChannelMessage)
|
||||
if (isOwnMessage)
|
||||
Icon(Icons.account_circle, size: 16, color: Theme.of(context).colorScheme.primary)
|
||||
else if (message.isChannelMessage)
|
||||
const Icon(Icons.tag, size: 16)
|
||||
else
|
||||
const Icon(Icons.person, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
message.displaySender,
|
||||
isOwnMessage ? 'You' : message.displaySender,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOwnMessage ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -735,6 +748,18 @@ class _MessageBubble extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Color _getMessageBubbleColor(BuildContext context, bool isOwnMessage, bool isDarkMode) {
|
||||
if (isOwnMessage) {
|
||||
// Own messages: slightly highlighted with primary color tint
|
||||
return isDarkMode
|
||||
? Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3)
|
||||
: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.15);
|
||||
} else {
|
||||
// Others' messages: default surface color
|
||||
return Theme.of(context).colorScheme.surfaceVariant;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getSarMarkerColor(BuildContext context, bool isDarkMode) {
|
||||
if (message.sarMarkerType == null) {
|
||||
return Theme.of(context).colorScheme.primaryContainer;
|
||||
|
||||
@@ -5,20 +5,34 @@ import '../meshcore_constants.dart';
|
||||
/// Callback types for connection events
|
||||
typedef OnConnectionStateCallback = void Function(bool isConnected);
|
||||
typedef OnErrorCallback = void Function(String error);
|
||||
typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts);
|
||||
|
||||
/// Manages BLE connection lifecycle
|
||||
/// Manages BLE connection lifecycle with automatic reconnection
|
||||
class BleConnectionManager {
|
||||
BluetoothDevice? _device;
|
||||
BluetoothCharacteristic? _rxCharacteristic;
|
||||
BluetoothCharacteristic? _txCharacteristic;
|
||||
bool _isConnected = false;
|
||||
|
||||
// Reconnection state
|
||||
bool _reconnectionEnabled = true;
|
||||
bool _isReconnecting = false;
|
||||
int _reconnectionAttempt = 0;
|
||||
Timer? _reconnectionTimer;
|
||||
StreamSubscription<BluetoothConnectionState>? _connectionStateSubscription;
|
||||
static const int _maxReconnectionAttempts = 5;
|
||||
static const List<int> _reconnectionDelaysMs = [1000, 2000, 3000, 5000, 10000]; // Exponential backoff
|
||||
|
||||
// Callbacks
|
||||
OnConnectionStateCallback? onConnectionStateChanged;
|
||||
OnErrorCallback? onError;
|
||||
OnReconnectionAttemptCallback? onReconnectionAttempt;
|
||||
|
||||
// Getters
|
||||
bool get isConnected => _isConnected;
|
||||
bool get isReconnecting => _isReconnecting;
|
||||
int get reconnectionAttempt => _reconnectionAttempt;
|
||||
int get maxReconnectionAttempts => _maxReconnectionAttempts;
|
||||
BluetoothDevice? get device => _device;
|
||||
BluetoothCharacteristic? get rxCharacteristic => _rxCharacteristic;
|
||||
BluetoothCharacteristic? get txCharacteristic => _txCharacteristic;
|
||||
@@ -138,9 +152,13 @@ class BleConnectionManager {
|
||||
print('✅ [BLE] Notifications enabled');
|
||||
|
||||
_isConnected = true;
|
||||
_reconnectionAttempt = 0; // Reset reconnection counter on successful connection
|
||||
print('🔵 [BLE] Notifying connection state change: connected');
|
||||
onConnectionStateChanged?.call(true);
|
||||
|
||||
// Monitor connection state for automatic reconnection
|
||||
_setupConnectionMonitoring();
|
||||
|
||||
print('✅✅✅ [BLE] Connection completed successfully!');
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -156,6 +174,11 @@ class BleConnectionManager {
|
||||
/// Disconnect from device
|
||||
Future<void> disconnect() async {
|
||||
try {
|
||||
print('🔴 [BLE] Disconnect requested by user');
|
||||
// Disable reconnection before disconnecting
|
||||
_reconnectionEnabled = false;
|
||||
_cancelReconnection();
|
||||
|
||||
await _device?.disconnect();
|
||||
_isConnected = false;
|
||||
_device = null;
|
||||
@@ -167,8 +190,126 @@ class BleConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup connection monitoring for automatic reconnection
|
||||
void _setupConnectionMonitoring() {
|
||||
print('🔵 [BLE] Setting up connection monitoring for device: ${_device?.platformName}');
|
||||
|
||||
// Cancel any existing subscription
|
||||
_connectionStateSubscription?.cancel();
|
||||
|
||||
// Monitor connection state changes
|
||||
_connectionStateSubscription = _device?.connectionState.listen((state) {
|
||||
print('🔔 [BLE] Connection state changed: $state');
|
||||
|
||||
if (state == BluetoothConnectionState.disconnected) {
|
||||
print('⚠️ [BLE] Device disconnected unexpectedly!');
|
||||
_isConnected = false;
|
||||
onConnectionStateChanged?.call(false);
|
||||
|
||||
// Attempt automatic reconnection if enabled
|
||||
if (_reconnectionEnabled && !_isReconnecting) {
|
||||
print('🔄 [BLE] Starting automatic reconnection...');
|
||||
_attemptReconnection();
|
||||
}
|
||||
} else if (state == BluetoothConnectionState.connected) {
|
||||
print('✅ [BLE] Device connected');
|
||||
_isConnected = true;
|
||||
_reconnectionAttempt = 0;
|
||||
_isReconnecting = false;
|
||||
onConnectionStateChanged?.call(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Attempt to reconnect to the device
|
||||
Future<void> _attemptReconnection() async {
|
||||
if (_device == null || _isReconnecting || !_reconnectionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isReconnecting = true;
|
||||
_reconnectionAttempt++;
|
||||
|
||||
print('🔄 [BLE] Reconnection attempt $_reconnectionAttempt of $_maxReconnectionAttempts');
|
||||
onReconnectionAttempt?.call(_reconnectionAttempt, _maxReconnectionAttempts);
|
||||
|
||||
if (_reconnectionAttempt > _maxReconnectionAttempts) {
|
||||
print('❌ [BLE] Max reconnection attempts reached. Giving up.');
|
||||
_isReconnecting = false;
|
||||
onError?.call('Connection lost. Max reconnection attempts ($_maxReconnectionAttempts) reached.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate delay with exponential backoff
|
||||
final delayIndex = (_reconnectionAttempt - 1).clamp(0, _reconnectionDelaysMs.length - 1);
|
||||
final delayMs = _reconnectionDelaysMs[delayIndex];
|
||||
|
||||
print('🔄 [BLE] Waiting ${delayMs}ms before reconnection attempt $_reconnectionAttempt...');
|
||||
|
||||
// Wait before attempting reconnection
|
||||
_reconnectionTimer = Timer(Duration(milliseconds: delayMs), () async {
|
||||
if (!_reconnectionEnabled) {
|
||||
print('🔄 [BLE] Reconnection cancelled by user');
|
||||
_isReconnecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
print('🔄 [BLE] Attempting to reconnect...');
|
||||
|
||||
// Try to reconnect
|
||||
final success = await connect(_device!);
|
||||
|
||||
if (success) {
|
||||
print('✅ [BLE] Reconnection successful!');
|
||||
_isReconnecting = false;
|
||||
_reconnectionAttempt = 0;
|
||||
} else {
|
||||
print('❌ [BLE] Reconnection attempt $_reconnectionAttempt failed');
|
||||
_isReconnecting = false;
|
||||
|
||||
// Try again if we haven't reached max attempts
|
||||
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
||||
_attemptReconnection();
|
||||
} else {
|
||||
onError?.call('Connection lost. Unable to reconnect after $_maxReconnectionAttempts attempts.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ [BLE] Reconnection attempt $_reconnectionAttempt error: $e');
|
||||
_isReconnecting = false;
|
||||
|
||||
// Try again if we haven't reached max attempts
|
||||
if (_reconnectionAttempt < _maxReconnectionAttempts) {
|
||||
_attemptReconnection();
|
||||
} else {
|
||||
onError?.call('Connection lost. Unable to reconnect: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Cancel ongoing reconnection attempts
|
||||
void _cancelReconnection() {
|
||||
print('🔴 [BLE] Cancelling reconnection attempts');
|
||||
_reconnectionTimer?.cancel();
|
||||
_reconnectionTimer = null;
|
||||
_isReconnecting = false;
|
||||
_reconnectionAttempt = 0;
|
||||
_connectionStateSubscription?.cancel();
|
||||
_connectionStateSubscription = null;
|
||||
}
|
||||
|
||||
/// Enable automatic reconnection (useful after user manually disconnects)
|
||||
void enableReconnection() {
|
||||
print('🔵 [BLE] Re-enabling automatic reconnection');
|
||||
_reconnectionEnabled = true;
|
||||
}
|
||||
|
||||
/// Dispose resources
|
||||
void dispose() {
|
||||
print('🔴 [BLE] Disposing BLE connection manager');
|
||||
_cancelReconnection();
|
||||
_device = null;
|
||||
_rxCharacteristic = null;
|
||||
_txCharacteristic = null;
|
||||
|
||||
@@ -30,6 +30,7 @@ typedef OnBinaryResponseCallback = void Function(Uint8List publicKeyPrefix, int
|
||||
typedef OnBatteryAndStorageCallback = void Function(int millivolts, int? usedKb, int? totalKb);
|
||||
typedef OnErrorCallback = void Function(String error);
|
||||
typedef OnConnectionStateCallback = void Function(bool isConnected);
|
||||
typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts);
|
||||
|
||||
/// MeshCore BLE Service - coordinates BLE communication components
|
||||
class MeshCoreBleService {
|
||||
@@ -40,6 +41,7 @@ class MeshCoreBleService {
|
||||
|
||||
// Event callbacks
|
||||
OnConnectionStateCallback? onConnectionStateChanged;
|
||||
OnReconnectionAttemptCallback? onReconnectionAttempt;
|
||||
OnContactCallback? onContactReceived;
|
||||
OnContactsCompleteCallback? onContactsComplete;
|
||||
OnMessageCallback? onMessageReceived;
|
||||
@@ -77,6 +79,10 @@ class MeshCoreBleService {
|
||||
_connectionManager.onError = (error) {
|
||||
onError?.call(error);
|
||||
};
|
||||
_connectionManager.onReconnectionAttempt = (attemptNumber, maxAttempts) {
|
||||
print('🔄 [Service] Reconnection attempt $attemptNumber/$maxAttempts');
|
||||
onReconnectionAttempt?.call(attemptNumber, maxAttempts);
|
||||
};
|
||||
|
||||
// Command sender callbacks
|
||||
_commandSender.onError = (error) {
|
||||
@@ -148,6 +154,9 @@ class MeshCoreBleService {
|
||||
|
||||
// Getters
|
||||
bool get isConnected => _connectionManager.isConnected;
|
||||
bool get isReconnecting => _connectionManager.isReconnecting;
|
||||
int get reconnectionAttempt => _connectionManager.reconnectionAttempt;
|
||||
int get maxReconnectionAttempts => _connectionManager.maxReconnectionAttempts;
|
||||
int get rxPacketCount => _responseHandler.rxPacketCount;
|
||||
int get txPacketCount => _commandSender.txPacketCount;
|
||||
List<BlePacketLog> get packetLogs {
|
||||
|
||||
@@ -111,6 +111,16 @@ class FrameParser {
|
||||
text = reader.readString();
|
||||
}
|
||||
|
||||
// Parse sender name from channel message format: "<sender_name>: <actual_message>"
|
||||
String? senderName;
|
||||
String actualMessage = text;
|
||||
|
||||
if (text.contains(': ')) {
|
||||
final colonIndex = text.indexOf(': ');
|
||||
senderName = text.substring(0, colonIndex);
|
||||
actualMessage = text.substring(colonIndex + 2); // Skip ": "
|
||||
}
|
||||
|
||||
return Message(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx',
|
||||
messageType: MessageType.channel,
|
||||
@@ -118,7 +128,8 @@ class FrameParser {
|
||||
pathLen: pathLen,
|
||||
textType: txtType,
|
||||
senderTimestamp: senderTimestamp,
|
||||
text: text,
|
||||
text: actualMessage, // Store the actual message without sender prefix
|
||||
senderName: senderName, // Store extracted sender name
|
||||
receivedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user