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.
This commit is contained in:
Janez T
2025-10-15 09:31:19 +02:00
parent 05b90d7f1f
commit 9124b53073
17 changed files with 5685 additions and 190 deletions

View File

@@ -0,0 +1,341 @@
# Messaging Improvements Implementation Summary
## Date: 2025-01-14
## Overview
This document summarizes the messaging improvements implemented based on the gap analysis in `MESSAGE_SEND_RECEIVE_GAP_ANALYSIS.md`.
## Gap Analysis Results
### Gap #1: Direct Messages UI
**Status**: ✅ ALREADY IMPLEMENTED
- **Location**: `lib/screens/contacts_tab.dart:351-937`
- Direct message UI exists via Contacts tab
- Users can tap message icon on chat contacts to open direct message sheet
- `_DirectMessageSheet` widget provides full message composition UI
- Messages are sent with delivery tracking via `sendTextMessage()`
### Gap #2: Timeout Handling
**Status**: ✅ NEWLY IMPLEMENTED
- **Files Modified**:
- `lib/providers/messages_provider.dart`
#### Implementation Details
**1. Added Timer Infrastructure** (lines 1, 19):
```dart
import 'dart:async';
// Track timeout timers for pending messages
final Map<int, Timer> _timeoutTimers = {};
```
**2. Start Timeout on Message Sent** (lines 280-291):
```dart
// Start timeout timer
_timeoutTimers[expectedAckTag] = Timer(
Duration(milliseconds: suggestedTimeoutMs),
() {
print('⏱️ [MessagesProvider] Timeout for message $messageId (ACK $expectedAckTag)');
if (_pendingSentMessages.containsKey(expectedAckTag)) {
markMessageFailed(messageId);
}
},
);
```
**3. Cancel Timeout on Delivery** (lines 312-314):
```dart
// Cancel timeout timer
_timeoutTimers[ackCode]?.cancel();
_timeoutTimers.remove(ackCode);
```
**4. Cancel Timeout on Manual Failure** (lines 338-341):
```dart
// Cancel timeout timer if it exists
if (message.expectedAckTag != null) {
_timeoutTimers[message.expectedAckTag]?.cancel();
_timeoutTimers.remove(message.expectedAckTag);
_pendingSentMessages.remove(message.expectedAckTag);
}
```
**5. Clean Up on Dispose** (lines 351-359):
```dart
@override
void dispose() {
// Cancel all pending timeout timers
for (final timer in _timeoutTimers.values) {
timer.cancel();
}
_timeoutTimers.clear();
super.dispose();
}
```
### Gap #3: Retry Logic
**Status**: ✅ NEWLY IMPLEMENTED
- **Files Modified**:
- `lib/screens/messages_tab.dart`
- `lib/providers/connection_provider.dart`
#### Implementation Details
**1. Retry Button UI** (lines 570-597 in messages_tab.dart):
```dart
// Show retry button for failed messages
if (message.deliveryStatus == MessageDeliveryStatus.failed) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () => _retryFailedMessage(context, message),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.orange, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.refresh, size: 12, color: Colors.orange),
const SizedBox(width: 4),
Text(
'Retry',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
```
**2. Retry Logic** (lines 400-479 in messages_tab.dart):
```dart
Future<void> _retryFailedMessage(BuildContext context, Message failedMessage) async {
// Check connection
if (!connectionProvider.deviceInfo.isConnected) {
// Show error
return;
}
// Check if max attempts reached (protocol supports 0-3, so 4 total attempts)
final currentAttempt = failedMessage.attemptNumber ?? 0;
if (currentAttempt >= 3) {
// Show max attempts reached error
return;
}
final nextAttempt = currentAttempt + 1;
final retryMessageId = '${failedMessage.id}_retry_$nextAttempt';
// Create retry message with updated attempt number
final retryMessage = failedMessage.copyWith(
id: retryMessageId,
deliveryStatus: MessageDeliveryStatus.sending,
attemptNumber: nextAttempt,
sentAt: DateTime.now(),
);
messagesProvider.addSentMessage(retryMessage);
// Resend the message
if (failedMessage.messageType == MessageType.channel) {
await connectionProvider.sendChannelMessage(
channelIdx: failedMessage.channelIdx ?? 0,
text: failedMessage.text,
messageId: retryMessageId,
attempt: nextAttempt,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Retrying message (attempt ${nextAttempt + 1}/4)...'),
backgroundColor: Colors.orange,
),
);
}
}
```
**3. Added Attempt Parameter to Connection Provider** (lines 400-468 in connection_provider.dart):
Updated `sendTextMessage()`:
```dart
Future<bool> sendTextMessage({
required Uint8List contactPublicKey,
required String text,
String? messageId,
int attempt = 0, // NEW: retry attempt number (0-3)
}) async {
await _bleService.sendTextMessage(
contactPublicKey: contactPublicKey,
text: text,
attempt: attempt, // NEW: pass to BLE service
);
if (messageId != null) {
_pendingSentMessageIds.add(messageId);
print(' Added message ID to pending queue: $messageId (attempt $attempt)');
}
}
```
Updated `sendChannelMessage()`:
```dart
Future<void> sendChannelMessage({
required int channelIdx,
required String text,
String? messageId, // NEW: track delivery
int attempt = 0, // NEW: retry attempt number (0-3)
}) async {
await _bleService.sendChannelMessage(
channelIdx: channelIdx,
text: text,
attempt: attempt, // NEW: pass to BLE service
);
// NEW: Track message ID for delivery confirmation
if (messageId != null) {
_pendingSentMessageIds.add(messageId);
print(' Added message ID to pending queue: $messageId (attempt $attempt)');
}
}
```
## How It Works
### Timeout Flow
1. User sends message → `addSentMessage()` called with `sending` status
2. BLE service sends message → receives `RESP_CODE_SENT` (code 6)
3. `markMessageSent()` called with ACK tag and timeout value
4. Timer started for specified timeout (e.g., 30000ms)
5. Two possible outcomes:
- **Success**: `PUSH_CODE_SEND_CONFIRMED` (0x82) arrives → `markMessageDelivered()` cancels timer → message marked `delivered`
- **Timeout**: Timer expires → message automatically marked `failed`
### Retry Flow
1. Message times out or fails → UI shows red "Failed" status with orange "Retry" button
2. User taps "Retry" button
3. Check attempt number (must be < 3, since protocol supports 0-3 = 4 total attempts)
4. Create new message with:
- New message ID: `{original_id}_retry_{attempt}`
- Status: `sending`
- Attempt number: `currentAttempt + 1`
5. Send message with new attempt number via BLE
6. New timeout timer started automatically
7. Process repeats until delivered or max attempts reached
## Protocol Compliance
All implementations follow the MeshCore BLE Companion Radio protocol:
- **Timeout values**: Use `suggestedTimeoutMs` from `RESP_CODE_SENT` (code 6)
- **Attempt numbers**: Range 0-3 (4 total attempts) as specified in protocol
- **Message tracking**: Use expected ACK tag from `RESP_CODE_SENT` to match with `PUSH_CODE_SEND_CONFIRMED` (0x82)
- **Delivery confirmation**: Round-trip time (RTT) stored from delivery confirmation
## Testing Checklist
### Timeout Handling
- [ ] Send message to unreachable contact
- [ ] Verify message shows "Sent" status initially
- [ ] Wait for timeout period (e.g., 30 seconds)
- [ ] Verify message automatically changes to "Failed" status
- [ ] Check logs for timeout message: `⏱️ [MessagesProvider] Timeout for message...`
### Retry Logic
- [ ] Cause a message to fail (send to non-existent contact or wait for timeout)
- [ ] Verify "Failed" status shows with orange "Retry" button
- [ ] Tap "Retry" button
- [ ] Verify new message appears with "Sending" status
- [ ] Verify snackbar shows "Retrying message (attempt 2/4)..."
- [ ] Repeat retry up to 4 total attempts
- [ ] On 4th attempt, verify "Retry" button disappears
- [ ] Attempt to retry again, verify error: "Maximum retry attempts reached"
### Delivery Success
- [ ] Send message to reachable contact
- [ ] Verify message shows "Sent" status
- [ ] Wait for delivery confirmation
- [ ] Verify message changes to "Delivered" status with green checkmarks
- [ ] Verify timeout timer was cancelled (no failure after timeout period)
- [ ] Check logs for delivery message: `✅ [MessagesProvider] Message {id} delivered in {ms}ms`
## Known Limitations
1. **Direct Message Retry**: Not yet implemented
- Retry button works only for channel messages
- Direct message retry would require looking up contact's full public key
- Shows "Direct message retry not yet implemented" message
2. **Automatic Retry**: Not implemented
- User must manually tap "Retry" button
- Future enhancement could add automatic retry with exponential backoff
3. **Retry Deduplication**: Messages show as separate entries
- Each retry creates a new message in the history
- Future enhancement could group retries under original message
## Files Changed
1. **lib/providers/messages_provider.dart**
- Added `dart:async` import
- Added `_timeoutTimers` map
- Modified `markMessageSent()` to start timers
- Modified `markMessageDelivered()` to cancel timers
- Modified `markMessageFailed()` to cancel timers
- Added `dispose()` method to clean up timers
2. **lib/providers/connection_provider.dart**
- Modified `sendTextMessage()` to accept `attempt` parameter
- Modified `sendChannelMessage()` to accept `messageId` and `attempt` parameters
- Both methods now track message IDs for delivery confirmation
3. **lib/screens/messages_tab.dart**
- Added retry button UI to `_MessageBubble` widget
- Added `_retryFailedMessage()` method
- Retry UI appears only for failed messages
- Shows attempt count (e.g., "attempt 2/4")
## Performance Impact
- **Memory**: Minimal - one Timer object per pending message
- **CPU**: Negligible - timers use OS-level scheduling
- **Network**: No change - only affects local message state management
## Future Enhancements
1. **Automatic Retry with Backoff**
- Implement exponential backoff (e.g., 5s, 10s, 20s, 40s)
- Configurable via settings
2. **Retry Grouping**
- Group retry attempts under original message
- Show retry history in message details
3. **Direct Message Retry**
- Add contact lookup by public key prefix
- Implement retry for direct messages
4. **Smart Timeout Adjustment**
- Learn from network conditions
- Adjust timeout based on historical RTT
5. **Batch Retry**
- "Retry All Failed" button
- Retry multiple failed messages at once
## Conclusion
The messaging system now has robust timeout handling and manual retry capabilities for channel messages. Messages automatically fail after the protocol-specified timeout period, and users can retry failed messages up to 4 times as allowed by the MeshCore protocol.
Direct messages can already be sent via the Contacts tab, so Gap #1 was already addressed. Gaps #2 and #3 are now fully implemented and ready for testing.