diff --git a/lib/models/message.dart b/lib/models/message.dart index fc4151b..488d62b 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -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, diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index dbcc8fc..a4dc371 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -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) { diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart index 12f240e..3791e9e 100644 --- a/lib/providers/connection_provider.dart +++ b/lib/providers/connection_provider.dart @@ -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 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 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(); diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index 3914687..2dcd55c 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -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 = []; + 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(); diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index e567139..a0acac7 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -335,28 +335,64 @@ class _HomeScreenState extends State 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(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( diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 1cb01f4..6ba828e 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -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(); + 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; diff --git a/lib/services/ble/ble_connection_manager.dart b/lib/services/ble/ble_connection_manager.dart index 0d51a66..4c5a254 100644 --- a/lib/services/ble/ble_connection_manager.dart +++ b/lib/services/ble/ble_connection_manager.dart @@ -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? _connectionStateSubscription; + static const int _maxReconnectionAttempts = 5; + static const List _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 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 _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; diff --git a/lib/services/meshcore_ble_service.dart b/lib/services/meshcore_ble_service.dart index ff9664b..f5adf78 100644 --- a/lib/services/meshcore_ble_service.dart +++ b/lib/services/meshcore_ble_service.dart @@ -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 get packetLogs { diff --git a/lib/services/protocol/frame_parser.dart b/lib/services/protocol/frame_parser.dart index 7b2524d..b90f5fd 100644 --- a/lib/services/protocol/frame_parser.dart +++ b/lib/services/protocol/frame_parser.dart @@ -111,6 +111,16 @@ class FrameParser { text = reader.readString(); } + // Parse sender name from channel message format: ": " + 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(), ); }