- 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.
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)
- ✅ Sending channel messages (public broadcast)
- ✅ Sending direct messages to rooms (persistent SAR markers)
- ✅ Receiving messages via
PUSH_CODE_MSG_WAITING - ✅ Message delivery tracking (sending → sent → delivered)
- ✅ SAR marker parsing and display
- ✅ Message persistence (MessageStorageService)
- ✅ Protocol compliance (all frame formats correct)
❌ What's Missing (Gaps)
- ❌ Sending direct messages to individual contacts (only room DMs work)
- ❌ Timeout handling for failed messages
- ❌ Message retry logic (automatic retries on failure)
- ❌ 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)
- ❌ 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)
- ❌ Gap #2: Timeout Handling
- Impact: Failed messages never show as failed
- Complexity: Low (timer logic)
- Effort: 1 hour
Priority 3: MEDIUM (Nice to Have)
- ❌ 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:
- ✅ Add recipient selector dropdown to messages tab
- ✅ Add
_sendDirectMessage()method - ✅ Wire to existing
sendTextMessage()BLE method - ✅ Test with team members
Files to Modify:
lib/screens/messages_tab.dart- Add
Contact? _selectedRecipientstate - Add recipient dropdown above message input
- Add
_sendDirectMessage()method - Update
_sendMessage()to route to channel vs contact
- Add
Estimated Time: 2-3 hours
Phase 2: Timeout Handling (Priority 2)
Goal: Mark messages as failed when timeout expires
Tasks:
- ✅ Add
Map<int, Timer> _timeoutTimersto MessagesProvider - ✅ Start timer in
markMessageSent() - ✅ Cancel timer in
markMessageDelivered() - ✅ Call
markMessageFailed()on timeout - ✅ 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:
- ✅ Add "Retry" button to failed message bubbles
- ✅ Add
_retryMessage()method - ✅ Test retry flow
Files to Modify:
lib/screens/messages_tab.dart- Add retry button to
_MessageBubblefor failed messages - Add
_retryMessage()callback
- Add retry button to
Estimated Time: 1 hour
Phase 4: Automatic Retry (Priority 3b) - OPTIONAL
Goal: Automatically retry failed messages
Tasks:
- ✅ Update
_handleMessageTimeout()to retry - ✅ Pass
attemptparameter through send chain - ✅ Test 4-attempt retry cycle
- ✅ Verify attempt 3 uses flood mode (per protocol)
Files to Modify:
lib/providers/messages_provider.dartlib/providers/connection_provider.dartlib/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 ✅
- ✅ Protocol implementation is 100% correct
- ✅ Delivery tracking infrastructure exists
- ✅ SAR markers work perfectly
- ✅ Room messages work
- ✅ Channel messages work
What Needs Adding ❌
- ❌ UI for direct messages to contacts (2-3 hours)
- ❌ Timeout timers (1 hour)
- ❌ Retry logic (2-3 hours)
Total Estimated Effort
Minimum Viable (Phase 1 + 2): 3-4 hours Full Featured (All phases): 6-9 hours
8. Recommended Next Steps
-
Immediate (Today): Implement Gap #1 (Direct Messages UI)
- This unlocks the core messaging functionality
- Users can finally message each other
-
Short Term (This Week): Implement Gap #2 (Timeout Handling)
- Improves reliability
- Users see when messages fail
-
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?