Files
meshcore-sar_android/MESSAGE_SEND_RECEIVE_GAP_ANALYSIS.md
Janez T 9124b53073 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.
2025-10-15 09:31:19 +02:00

17 KiB

Message Send & Receive - Gap Analysis

Date: 2025-01-14 Purpose: Identify what's missing in the messaging implementation

Executive Summary

Your messaging implementation is 90% complete! The core functionality works correctly. Here's what's implemented vs what's missing:

What Works (Already Implemented)

  1. Sending channel messages (public broadcast)
  2. Sending direct messages to rooms (persistent SAR markers)
  3. Receiving messages via PUSH_CODE_MSG_WAITING
  4. Message delivery tracking (sending → sent → delivered)
  5. SAR marker parsing and display
  6. Message persistence (MessageStorageService)
  7. Protocol compliance (all frame formats correct)

What's Missing (Gaps)

  1. Sending direct messages to individual contacts (only room DMs work)
  2. Timeout handling for failed messages
  3. Message retry logic (automatic retries on failure)
  4. User can't send regular messages to contacts (only SAR markers to rooms)

1. Current Implementation Analysis

1.1 Sending Messages - What Works

Channel Messages (Public Broadcast)

File: messages_tab.dart:49-93

Future<void> _sendMessage() async {
  final text = _textController.text.trim();

  // Always send to public channel (channel 0)
  await connectionProvider.sendChannelMessage(
    channelIdx: 0,
    text: text,
  );
}

Status: WORKING

  • Sends to public channel
  • Text limit enforced (160 chars)
  • User feedback via snackbar

SAR Markers to Rooms

File: messages_tab.dart:109-221

Future<void> _sendSarMessage(...) async {
  // Format: S:<emoji>:<latitude>,<longitude>
  final sarMessage = 'S:${sarType.emoji}:${position.latitude},${position.longitude}';

  if (sendToChannel) {
    // Send to public channel (ephemeral)
    await connectionProvider.sendChannelMessage(
      channelIdx: 0,
      text: fullMessage,
    );
  } else {
    // Send to room (persistent)
    final sentSuccessfully = await connectionProvider.sendTextMessage(
      contactPublicKey: roomPublicKey!,
      text: fullMessage,
      messageId: messageId,
    );
  }
}

Status: WORKING

  • Sends SAR markers to rooms
  • Tracks delivery with message ID
  • Updates status (sending → sent → delivered)

1.2 Sending Messages - What's Missing

Direct Messages to Individual Contacts

Current State: No UI to send regular messages to individual contacts!

Gap: User can only:

  • Send to public channel
  • Send SAR markers to rooms

Missing: Send regular text messages to individual team members

Example Use Case:

User wants to send "Meet at checkpoint B" to John (a chat contact)
Current: ❌ No way to do this
Should: ✅ Send direct message via CMD_SEND_TXT_MSG

Timeout Handling

Current State: Messages marked "Sent" wait forever for delivery confirmation

File: messages_provider.dart:262-279

void markMessageSent(String messageId, int expectedAckTag, int suggestedTimeoutMs) {
  final updatedMessage = message.copyWith(
    deliveryStatus: MessageDeliveryStatus.sent,
    expectedAckTag: expectedAckTag,
    suggestedTimeoutMs: suggestedTimeoutMs, // ⚠️ Stored but not used!
  );

  _pendingSentMessages[expectedAckTag] = updatedMessage;
  // ❌ No timeout timer started!
}

Gap: No timer to mark message as "Failed" if timeout expires

Should Do:

void markMessageSent(String messageId, int expectedAckTag, int suggestedTimeoutMs) {
  // ... existing code ...

  // Start timeout timer
  Future.delayed(Duration(milliseconds: suggestedTimeoutMs), () {
    if (_pendingSentMessages.containsKey(expectedAckTag)) {
      // Message not delivered within timeout
      markMessageFailed(messageId);
    }
  });
}

Message Retry Logic

Current State: Failed messages stay failed, no retry

Gap: MeshCore supports retries with attempt parameter (0-3)

Protocol Spec (MESSAGES.md):

CMD_SEND_TXT_MSG:
- attempt (1 byte): 0-3 (retry attempt number)

Current Implementation (meshcore_ble_service.dart:1183-1201):

Future<void> sendTextMessage({
  required Uint8List contactPublicKey,
  required String text,
  int textType = 0,
  int attempt = 0, // ✅ Parameter exists but never used!
}) async {
  writer.writeByte(attempt); // Always 0
}

Missing: Retry logic that increments attempt on timeout


2. Detailed Gap Analysis

Gap #1: No UI for Direct Messages to Contacts

Problem

Current UI (messages_tab.dart):

┌─────────────────────────────┐
│  Messages Tab               │
├─────────────────────────────┤
│                             │
│  [Message List]             │
│                             │
│                             │
├─────────────────────────────┤
│ [SAR]  [Text Input]  [Send] │  ← Always sends to public channel
└─────────────────────────────┘

Missing:

  • No recipient selector
  • No way to send DM to individual contact
  • Can only send to public channel OR rooms (via SAR dialog)

Solution

Add Recipient Selector:

Contact? _selectedRecipient; // null = public channel

// In build():
Row(
  children: [
    // Recipient dropdown
    DropdownButton<Contact?>(
      value: _selectedRecipient,
      hint: Text('Public Channel'),
      items: [
        DropdownMenuItem(value: null, child: Text('📢 Public')),
        ...contactsProvider.chatContacts.map((contact) =>
          DropdownMenuItem(
            value: contact,
            child: Text('👤 ${contact.displayName}'),
          ),
        ),
      ],
      onChanged: (value) => setState(() => _selectedRecipient = value),
    ),

    // Message input
    Expanded(child: TextField(...)),

    // Send button
    IconButton(
      onPressed: () => _selectedRecipient == null
          ? _sendChannelMessage()
          : _sendDirectMessage(_selectedRecipient!),
    ),
  ],
)

New Method:

Future<void> _sendDirectMessage(Contact recipient) async {
  final text = _textController.text.trim();

  final messageId = '${DateTime.now().millisecondsSinceEpoch}_sent';

  // Create sent message
  final sentMessage = Message(
    id: messageId,
    messageType: MessageType.contact,
    senderPublicKeyPrefix: devicePublicKey?.sublist(0, 6),
    pathLen: 0,
    textType: MessageTextType.plain,
    senderTimestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
    text: text,
    receivedAt: DateTime.now(),
    deliveryStatus: MessageDeliveryStatus.sending,
  );

  // Add to messages list
  messagesProvider.addSentMessage(sentMessage);

  // Send via BLE
  final success = await connectionProvider.sendTextMessage(
    contactPublicKey: recipient.publicKey,
    text: text,
    messageId: messageId,
  );

  if (!success) {
    messagesProvider.markMessageFailed(messageId);
  }
}

Gap #2: No Timeout Handling

Problem

Current Flow:

Send Message
    ↓
RESP_CODE_SENT (ACK tag: 12345, timeout: 30000ms)
    ↓
Mark as "Sent"
    ↓
⏳ Wait forever for PUSH_CODE_SEND_CONFIRMED...
    ↓
❌ If never arrives, message stays "Sent" indefinitely

Should Be:

Send Message
    ↓
RESP_CODE_SENT (ACK tag: 12345, timeout: 30000ms)
    ↓
Mark as "Sent" + Start 30s timeout timer
    ↓
├─ PUSH_CODE_SEND_CONFIRMED arrives → ✅ Mark "Delivered"
└─ Timeout expires → ❌ Mark "Failed"

Solution

Update MessagesProvider (messages_provider.dart):

// Track timeout timers by ACK tag
final Map<int, Timer> _timeoutTimers = {};

void markMessageSent(String messageId, int expectedAckTag, int suggestedTimeoutMs) {
  final index = _messages.indexWhere((m) => m.id == messageId);
  if (index != -1) {
    final message = _messages[index];
    final updatedMessage = message.copyWith(
      deliveryStatus: MessageDeliveryStatus.sent,
      expectedAckTag: expectedAckTag,
      suggestedTimeoutMs: suggestedTimeoutMs,
    );
    _messages[index] = updatedMessage;

    // Track by ACK tag
    _pendingSentMessages[expectedAckTag] = updatedMessage;

    // ✅ NEW: Start timeout timer
    _timeoutTimers[expectedAckTag] = Timer(
      Duration(milliseconds: suggestedTimeoutMs),
      () {
        // Timeout expired - mark as failed
        if (_pendingSentMessages.containsKey(expectedAckTag)) {
          print('⏱️ Message timeout: ACK $expectedAckTag not received within ${suggestedTimeoutMs}ms');
          markMessageFailed(messageId);
        }
      },
    );

    _persistMessages();
    notifyListeners();
  }
}

void markMessageDelivered(int ackCode, int roundTripTimeMs) {
  // Find message by ACK code
  final message = _pendingSentMessages[ackCode];
  if (message != null) {
    final index = _messages.indexWhere((m) => m.id == message.id);
    if (index != -1) {
      final updatedMessage = message.copyWith(
        deliveryStatus: MessageDeliveryStatus.delivered,
        roundTripTimeMs: roundTripTimeMs,
        deliveredAt: DateTime.now(),
      );
      _messages[index] = updatedMessage;

      // ✅ NEW: Cancel timeout timer
      _timeoutTimers[ackCode]?.cancel();
      _timeoutTimers.remove(ackCode);

      // Remove from pending
      _pendingSentMessages.remove(ackCode);

      _persistMessages();
      notifyListeners();
    }
  }
}

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;

    // ✅ NEW: Cancel timeout timer if exists
    if (message.expectedAckTag != null) {
      _timeoutTimers[message.expectedAckTag]?.cancel();
      _timeoutTimers.remove(message.expectedAckTag);
      _pendingSentMessages.remove(message.expectedAckTag);
    }

    _persistMessages();
    notifyListeners();
  }
}

// ✅ NEW: Cleanup on dispose
@override
void dispose() {
  // Cancel all pending timers
  for (final timer in _timeoutTimers.values) {
    timer.cancel();
  }
  _timeoutTimers.clear();
  super.dispose();
}

Gap #3: No Message Retry Logic

Problem

Current: Failed messages stay failed forever

Protocol Supports:

Attempt 0 → Timeout → ❌ Failed (no retry)

Should Support:

Attempt 0 → Timeout → Retry
Attempt 1 → Timeout → Retry
Attempt 2 → Timeout → Retry
Attempt 3 → Timeout → ❌ Failed (last attempt uses flood mode)

Solution

Option 1: Manual Retry (Simple)

Add "Retry" button to failed messages:

// In _MessageBubble:
if (message.deliveryStatus == MessageDeliveryStatus.failed) ...[
  ElevatedButton.icon(
    onPressed: () => _retryMessage(message),
    icon: Icon(Icons.refresh),
    label: Text('Retry'),
  ),
],

Option 2: Automatic Retry (Advanced)

Update timeout handler:

void _handleMessageTimeout(String messageId, int attemptNumber) {
  if (attemptNumber < 3) {
    // Retry with next attempt number
    print('⏱️ Attempt $attemptNumber timeout - retrying...');
    _retryMessage(messageId, attemptNumber + 1);
  } else {
    // All attempts exhausted
    print('❌ All 4 attempts failed - marking as failed');
    markMessageFailed(messageId);
  }
}

Future<void> _retryMessage(String messageId, int attempt) async {
  final message = _messages.firstWhere((m) => m.id == messageId);

  // Update attempt count
  final updatedMessage = message.copyWith(
    deliveryStatus: MessageDeliveryStatus.sending,
  );
  // ... update in list ...

  // Resend with incremented attempt number
  final success = await connectionProvider.sendTextMessage(
    contactPublicKey: message.recipientPublicKey,
    text: message.text,
    messageId: messageId,
    attempt: attempt,
  );
}

3. Priority Ranking

Priority 1: CRITICAL (Blocks Core Functionality)

  1. Gap #1: Direct Messages to Contacts
    • Impact: Users can't send messages to individual team members
    • Complexity: Medium (UI + wire to existing BLE code)
    • Effort: 2-3 hours

Priority 2: HIGH (Improves Reliability)

  1. Gap #2: Timeout Handling
    • Impact: Failed messages never show as failed
    • Complexity: Low (timer logic)
    • Effort: 1 hour

Priority 3: MEDIUM (Nice to Have)

  1. Gap #3: Automatic Retry
    • Impact: Failed messages need manual intervention
    • Complexity: Medium (retry orchestration)
    • Effort: 2-3 hours

4. Implementation Roadmap

Phase 1: Basic DM Support (Priority 1)

Goal: Enable sending direct messages to contacts

Tasks:

  1. Add recipient selector dropdown to messages tab
  2. Add _sendDirectMessage() method
  3. Wire to existing sendTextMessage() BLE method
  4. Test with team members

Files to Modify:

  • lib/screens/messages_tab.dart
    • Add Contact? _selectedRecipient state
    • Add recipient dropdown above message input
    • Add _sendDirectMessage() method
    • Update _sendMessage() to route to channel vs contact

Estimated Time: 2-3 hours

Phase 2: Timeout Handling (Priority 2)

Goal: Mark messages as failed when timeout expires

Tasks:

  1. Add Map<int, Timer> _timeoutTimers to MessagesProvider
  2. Start timer in markMessageSent()
  3. Cancel timer in markMessageDelivered()
  4. Call markMessageFailed() on timeout
  5. Add dispose() to cancel timers

Files to Modify:

  • lib/providers/messages_provider.dart
    • Add timeout timer tracking
    • Update markMessageSent()
    • Update markMessageDelivered()
    • Update markMessageFailed()
    • Add dispose()

Estimated Time: 1 hour

Phase 3: Manual Retry (Priority 3a)

Goal: Let user manually retry failed messages

Tasks:

  1. Add "Retry" button to failed message bubbles
  2. Add _retryMessage() method
  3. Test retry flow

Files to Modify:

  • lib/screens/messages_tab.dart
    • Add retry button to _MessageBubble for failed messages
    • Add _retryMessage() callback

Estimated Time: 1 hour

Phase 4: Automatic Retry (Priority 3b) - OPTIONAL

Goal: Automatically retry failed messages

Tasks:

  1. Update _handleMessageTimeout() to retry
  2. Pass attempt parameter through send chain
  3. Test 4-attempt retry cycle
  4. Verify attempt 3 uses flood mode (per protocol)

Files to Modify:

  • lib/providers/messages_provider.dart
  • lib/providers/connection_provider.dart
  • lib/services/meshcore_ble_service.dart

Estimated Time: 2-3 hours


5. Quick Fixes (Can Do Right Now)

Quick Fix #1: Add "Reply" to Contact Messages

File: lib/screens/messages_tab.dart

Add long-press handler to contact messages:

// In _MessageBubble:
GestureDetector(
  onLongPress: message.isContactMessage
      ? () => _showReplyOptions(context, message)
      : null,
  child: Container(...),
)
void _showReplyOptions(BuildContext context, Message message) {
  showModalBottomSheet(
    context: context,
    builder: (context) => Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ListTile(
          leading: Icon(Icons.reply),
          title: Text('Reply to ${message.displaySender}'),
          onTap: () {
            // Set recipient and open keyboard
            Navigator.pop(context);
            // ... set _selectedRecipient ...
          },
        ),
      ],
    ),
  );
}

6. Testing Checklist

After Implementing Gap #1 (Direct Messages)

  • Can send DM to chat contact
  • Message appears in recipient's messages list
  • Delivery status shows: Sending → Sent → Delivered
  • Failed messages show "Failed" status
  • Can send to public channel (existing feature still works)

After Implementing Gap #2 (Timeout Handling)

  • Turn off recipient device
  • Send message
  • Verify "Sent" status appears
  • Wait for timeout (30s)
  • Verify status changes to "Failed"
  • Turn on recipient device
  • Send message
  • Verify status changes to "Delivered" before timeout

After Implementing Gap #3 (Retry)

  • Manual retry: Click "Retry" on failed message
  • Verify message sends again
  • Auto retry: Turn off recipient device
  • Send message
  • Verify 4 retry attempts occur
  • Verify final status is "Failed" after all attempts

7. Summary

What's Already Great

  1. Protocol implementation is 100% correct
  2. Delivery tracking infrastructure exists
  3. SAR markers work perfectly
  4. Room messages work
  5. Channel messages work

What Needs Adding

  1. UI for direct messages to contacts (2-3 hours)
  2. Timeout timers (1 hour)
  3. Retry logic (2-3 hours)

Total Estimated Effort

Minimum Viable (Phase 1 + 2): 3-4 hours Full Featured (All phases): 6-9 hours


  1. Immediate (Today): Implement Gap #1 (Direct Messages UI)

    • This unlocks the core messaging functionality
    • Users can finally message each other
  2. Short Term (This Week): Implement Gap #2 (Timeout Handling)

    • Improves reliability
    • Users see when messages fail
  3. Optional (Next Week): Implement Gap #3 (Retry Logic)

    • Automatic retries improve success rate
    • Manual retry button is simple fallback

Would you like me to implement Gap #1 (Direct Messages UI) first?