From 2bc0e21cf0f5b52c64ce67c8c49a1e0155d5fe4b Mon Sep 17 00:00:00 2001 From: Janez T Date: Sat, 7 Mar 2026 09:23:29 +0100 Subject: [PATCH] Remove 100ms BLE command delay --- lib/models/message_reception_details.dart | 23 +++ lib/providers/app_provider.dart | 152 +++------------- .../helpers/raw_session_retransmit.dart | 21 --- lib/providers/image_provider.dart | 7 - lib/providers/voice_provider.dart | 7 - .../messages/image_message_bubble.dart | 3 + lib/widgets/messages/message_bubble.dart | 163 +++++++++++------- .../messages/message_bubble_signal.dart | 79 +++++++++ .../messages/voice_message_bubble.dart | 3 + pubspec.lock | 8 +- pubspec.yaml | 2 + .../message_reception_details_test.dart | 23 +++ .../helpers/raw_session_retransmit_test.dart | 43 +---- 13 files changed, 260 insertions(+), 274 deletions(-) diff --git a/lib/models/message_reception_details.dart b/lib/models/message_reception_details.dart index 90d264f..40cc70d 100644 --- a/lib/models/message_reception_details.dart +++ b/lib/models/message_reception_details.dart @@ -1,3 +1,26 @@ +const int _transmitEstimateToleranceMs = 1500; + +int? sanitizeEstimatedTransmitMs({ + required int? estimatedTransmitMs, + required int? senderToReceiptMs, +}) { + if (estimatedTransmitMs == null || estimatedTransmitMs <= 0) { + return null; + } + + if (senderToReceiptMs == null || senderToReceiptMs <= 0) { + return estimatedTransmitMs; + } + + // Sender timestamps are second-granularity, so allow a small cushion before + // treating the estimate as impossible for the observed delivery time. + if (estimatedTransmitMs > senderToReceiptMs + _transmitEstimateToleranceMs) { + return null; + } + + return estimatedTransmitMs; +} + class MessageReceptionDetails { final DateTime capturedAt; final DateTime? packetLoggedAt; diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index f9cc746..c29e267 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -70,10 +70,6 @@ class AppProvider with ChangeNotifier { final Map _voiceMissingRetryAttempts = {}; final Map _imageMissingRetryTimers = {}; final Map _imageMissingRetryAttempts = {}; - final FragmentAckWaitRegistry _voiceFragmentAckWaiters = - FragmentAckWaitRegistry(); - final FragmentAckWaitRegistry _imageFragmentAckWaiters = - FragmentAckWaitRegistry(); final FragmentAckWaitRegistry _rawProbeWaiters = FragmentAckWaitRegistry(); final Map> _pendingRawRouteProbes = {}; Timer? _packetCaptureFlushTimer; @@ -468,27 +464,6 @@ class AppProvider with ChangeNotifier { payload: payload, ); }; - voiceProvider.waitForFragmentAckCallback = - ({ - required sessionId, - required index, - timeout = const Duration(seconds: 8), - }) => _waitForVoiceFragmentAck( - sessionId: sessionId, - index: index, - timeout: timeout, - ); - imageProvider.waitForFragmentAckCallback = - ({ - required sessionId, - required index, - timeout = const Duration(seconds: 8), - }) => _waitForImageFragmentAck( - sessionId: sessionId, - index: index, - timeout: timeout, - ); - // When a contact is received from BLE connectionProvider.onContactReceived = (contact) { // Pass device public key to filter out our own contact @@ -814,18 +789,27 @@ class AppProvider with ChangeNotifier { connectionProvider.onRawDataReceived = (payload, snrRaw, rssiDbm) { final rawProbeRequest = RawRouteProbeRequest.tryParseBinary(payload); if (rawProbeRequest != null) { + debugPrint( + '📡 [AppProvider] Incoming raw route probe: nonce=${rawProbeRequest.nonce.toRadixString(16)} requester=${rawProbeRequest.requesterKey6}', + ); _handleRawRouteProbeRequest(rawProbeRequest); return; } final rawProbeAck = RawRouteProbeAck.tryParseBinary(payload); if (rawProbeAck != null) { + debugPrint( + '📡 [AppProvider] Incoming raw route probe ACK: nonce=${rawProbeAck.nonce.toRadixString(16)}', + ); _completeRawRouteProbeAck(rawProbeAck.nonce); return; } final voiceFetchRequest = VoiceFetchRequest.tryParseBinary(payload); if (voiceFetchRequest != null) { + debugPrint( + 'đŸŽ™ī¸ [AppProvider] Incoming voice fetch request: session=${voiceFetchRequest.sessionId} want=${voiceFetchRequest.want} requester=${voiceFetchRequest.requesterKey6}', + ); final requester = _resolveVoiceFetchRequester(voiceFetchRequest); if (requester == null) { debugPrint( @@ -906,18 +890,6 @@ class AppProvider with ChangeNotifier { return; } - final voiceAck = VoiceFragmentAck.tryParseBinary(payload); - if (voiceAck != null) { - _completeVoiceFragmentAck(voiceAck.sessionId, voiceAck.index); - return; - } - - final imageAck = ImageFragmentAck.tryParseBinary(payload); - if (imageAck != null) { - _completeImageFragmentAck(imageAck.sessionId, imageAck.index); - return; - } - if (ImagePacket.isImageBinary(payload)) { final frag = ImagePacket.tryParseBinary(payload); if (frag == null) return; @@ -928,7 +900,6 @@ class AppProvider with ChangeNotifier { width: session?.width ?? 0, height: session?.height ?? 0, ); - _sendImageFragmentAck(frag); _scheduleImageMissingRetry( frag.sessionId, justComplete: imageProvider.isComplete(frag.sessionId), @@ -941,7 +912,6 @@ class AppProvider with ChangeNotifier { if (pkt == null) return; debugPrint('đŸŽ™ī¸ [AppProvider] Binary voice packet received: $pkt'); final justComplete = voiceProvider.addPacket(pkt); - _sendVoiceFragmentAck(pkt); _scheduleVoiceMissingRetry(pkt.sessionId, justComplete: justComplete); // Insert or update the placeholder message in the chat list _handleIncomingVoicePacket(pkt, justComplete: justComplete); @@ -1586,7 +1556,6 @@ class AppProvider with ChangeNotifier { messagesProvider.addMessage(placeholder, contactLookup: (_) => ''); } - String _fragmentAckKey(String sessionId, int index) => '$sessionId:$index'; String _rawProbeKey(int nonce) => nonce.toRadixString(16).padLeft(8, '0').toLowerCase(); @@ -1627,6 +1596,9 @@ class AppProvider with ChangeNotifier { ); try { + debugPrint( + '📡 [AppProvider] Outgoing raw route probe: target=${target.advName} hops=${target.outPathLen} nonce=${nonce.toRadixString(16)}', + ); await connectionProvider.sendRawVoicePacket( contactPath: target.outPath, contactPathLen: target.outPathLen, @@ -1660,54 +1632,6 @@ class AppProvider with ChangeNotifier { return 'name:${target.advName}:${target.outPathLen}:${target.outPath.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}'; } - Future _waitForVoiceFragmentAck({ - required String sessionId, - required int index, - Duration timeout = const Duration(seconds: 8), - }) => _voiceFragmentAckWaiters.waitFor( - _fragmentAckKey(sessionId, index), - timeout: timeout, - ); - - void _completeVoiceFragmentAck(String sessionId, int index) { - final completed = _voiceFragmentAckWaiters.complete( - _fragmentAckKey(sessionId, index), - ); - if (completed == 0) { - debugPrint( - 'â„šī¸ [AppProvider] Voice fragment ACK had no waiter: $sessionId#$index', - ); - return; - } - debugPrint( - '✅ [AppProvider] Voice fragment ACK received for $sessionId#$index ($completed waiter(s))', - ); - } - - Future _waitForImageFragmentAck({ - required String sessionId, - required int index, - Duration timeout = const Duration(seconds: 8), - }) => _imageFragmentAckWaiters.waitFor( - _fragmentAckKey(sessionId, index), - timeout: timeout, - ); - - void _completeImageFragmentAck(String sessionId, int index) { - final completed = _imageFragmentAckWaiters.complete( - _fragmentAckKey(sessionId, index), - ); - if (completed == 0) { - debugPrint( - 'â„šī¸ [AppProvider] Image fragment ACK had no waiter: $sessionId#$index', - ); - return; - } - debugPrint( - '✅ [AppProvider] Image fragment ACK received for $sessionId#$index ($completed waiter(s))', - ); - } - void _handleRawRouteProbeRequest(RawRouteProbeRequest request) { final requester = _resolveContactByPrefixHex(request.requesterKey6); if (requester == null) { @@ -1726,6 +1650,9 @@ class AppProvider with ChangeNotifier { if (requester.outPath.isEmpty) { return; } + debugPrint( + '📡 [AppProvider] Outgoing raw route probe ACK: requester=${requester.advName} hops=${requester.outPathLen} nonce=${request.nonce.toRadixString(16)}', + ); unawaited( connectionProvider.sendRawVoicePacket( contactPath: requester.outPath, @@ -1739,46 +1666,6 @@ class AppProvider with ChangeNotifier { _rawProbeWaiters.complete(_rawProbeKey(nonce)); } - void _sendVoiceFragmentAck(VoicePacket packet) { - final senderKey6 = _voiceSessionSenderKey6[packet.sessionId]; - if (senderKey6 == null) return; - final sender = _resolveContactByPrefixHex(senderKey6); - if (sender == null) return; - if (sender.outPathLen < 0 || sender.outPathLen > _maxDirectPayloadHops) { - return; - } - unawaited( - connectionProvider.sendRawVoicePacket( - contactPath: sender.outPath, - contactPathLen: sender.outPathLen, - payload: VoiceFragmentAck( - sessionId: packet.sessionId, - index: packet.index, - ).encodeBinary(), - ), - ); - } - - void _sendImageFragmentAck(ImagePacket fragment) { - final senderKey6 = _imageSessionSenderKey6[fragment.sessionId]; - if (senderKey6 == null) return; - final sender = _resolveContactByPrefixHex(senderKey6); - if (sender == null) return; - if (sender.outPathLen < 0 || sender.outPathLen > _maxDirectPayloadHops) { - return; - } - unawaited( - connectionProvider.sendRawVoicePacket( - contactPath: sender.outPath, - contactPathLen: sender.outPathLen, - payload: ImageFragmentAck( - sessionId: fragment.sessionId, - index: fragment.index, - ).encodeBinary(), - ), - ); - } - MessageReceptionDetails? _buildReceptionDetailsSnapshot(Message message) { final matchedRxLog = _findBestMatchingRxLog(message); final estimatedTx = estimateMessageTransmitDuration( @@ -1788,9 +1675,12 @@ class AppProvider with ChangeNotifier { radioCr: connectionProvider.deviceInfo.radioCr, ); final senderToReceiptMs = _senderToReceiptMs(message); - final estimatedTransmitMs = estimatedTx > Duration.zero - ? estimatedTx.inMilliseconds - : null; + final estimatedTransmitMs = sanitizeEstimatedTransmitMs( + estimatedTransmitMs: estimatedTx > Duration.zero + ? estimatedTx.inMilliseconds + : null, + senderToReceiptMs: senderToReceiptMs, + ); final postTransmitDelayMs = senderToReceiptMs != null && estimatedTransmitMs != null ? (senderToReceiptMs - estimatedTransmitMs).clamp(0, 86400000).toInt() diff --git a/lib/providers/helpers/raw_session_retransmit.dart b/lib/providers/helpers/raw_session_retransmit.dart index fb8f1c5..60ca230 100644 --- a/lib/providers/helpers/raw_session_retransmit.dart +++ b/lib/providers/helpers/raw_session_retransmit.dart @@ -9,13 +9,6 @@ typedef RawPacketSender = required Uint8List payload, }); -typedef FragmentAckWaiter = - Future Function({ - required String sessionId, - required int index, - Duration timeout, - }); - Future serveCachedSessionFragments({ required String providerLabel, required String sessionId, @@ -25,9 +18,7 @@ Future serveCachedSessionFragments({ required int Function(T fragment) indexOf, required Uint8List Function(T fragment) encodeBinary, required RawPacketSender? sendRawPacket, - FragmentAckWaiter? waitForFragmentAck, Set? requestedIndices, - Duration ackTimeout = const Duration(seconds: 8), }) async { if (fragments.isEmpty) { debugPrint('âš ī¸ [$providerLabel] No cached fragments for $sessionId'); @@ -65,24 +56,12 @@ Future serveCachedSessionFragments({ continue; } try { - final ackFuture = waitForFragmentAck?.call( - sessionId: sessionId, - index: index, - timeout: ackTimeout, - ); await sendRawPacket( contactPath: requester.outPath, contactPathLen: requester.outPathLen, payload: encodeBinary(fragment), ); servedCount++; - if (ackFuture != null) { - final acked = await ackFuture; - if (!acked) { - debugPrint('âš ī¸ [$providerLabel] ACK timeout for $sessionId#$index'); - return false; - } - } } catch (e, st) { debugPrint( '❌ [$providerLabel] Serve error for $sessionId#$index: $e\n$st', diff --git a/lib/providers/image_provider.dart b/lib/providers/image_provider.dart index 00a02a7..2d46517 100644 --- a/lib/providers/image_provider.dart +++ b/lib/providers/image_provider.dart @@ -70,12 +70,6 @@ class ImageProvider with ChangeNotifier { required Uint8List payload, })? sendRawPacketCallback; - Future Function({ - required String sessionId, - required int index, - Duration timeout, - })? - waitForFragmentAckCallback; ImageProvider() { _restore(); @@ -264,7 +258,6 @@ class ImageProvider with ChangeNotifier { indexOf: (fragment) => fragment.index, encodeBinary: (fragment) => fragment.encodeBinary(), sendRawPacket: sendRawPacketCallback, - waitForFragmentAck: waitForFragmentAckCallback, requestedIndices: requestedIndices, ); } diff --git a/lib/providers/voice_provider.dart b/lib/providers/voice_provider.dart index 96bde9c..343d18c 100644 --- a/lib/providers/voice_provider.dart +++ b/lib/providers/voice_provider.dart @@ -71,12 +71,6 @@ class VoiceProvider with ChangeNotifier { required Uint8List payload, })? sendRawPacketCallback; - Future Function({ - required String sessionId, - required int index, - Duration timeout, - })? - waitForFragmentAckCallback; final Map _outgoingSessions = {}; @@ -217,7 +211,6 @@ class VoiceProvider with ChangeNotifier { indexOf: (packet) => packet.index, encodeBinary: (packet) => packet.encodeBinary(), sendRawPacket: sendRawPacketCallback, - waitForFragmentAck: waitForFragmentAckCallback, requestedIndices: requestedIndices, ); } diff --git a/lib/widgets/messages/image_message_bubble.dart b/lib/widgets/messages/image_message_bubble.dart index fe17b38..df3c672 100644 --- a/lib/widgets/messages/image_message_bubble.dart +++ b/lib/widgets/messages/image_message_bubble.dart @@ -406,6 +406,9 @@ class _ImageMessageBubbleState extends State { final payload = request.encodeBinary(); try { + debugPrint( + '📷 [ImageMessageBubble] Outgoing image fetch request: session=${envelope.sessionId} want=${isPartialResume ? 'missing' : 'all'} target=${sender.advName} hops=${sender.outPathLen}', + ); await conn.sendRawVoicePacket( contactPath: sender.outPath, contactPathLen: sender.outPathLen, diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index 8d30ea8..ee88752 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -22,6 +22,7 @@ import '../../utils/sar_message_parser.dart'; import '../../utils/key_comparison.dart'; import '../../utils/voice_message_parser.dart'; import '../../utils/image_message_parser.dart'; +import '../../utils/message_airtime_estimator.dart'; import '../../utils/tictactoe_message_parser.dart'; import '../../utils/location_formats.dart'; import '../../l10n/app_localizations.dart'; @@ -2465,74 +2466,114 @@ class _MessageBubbleState extends State { // Show single message delivery status else if (!message.isChannelMessage || message.deliveryStatus == MessageDeliveryStatus.failed) - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Icon( - getDeliveryStatusIcon(message.deliveryStatus), - size: 12, - color: getDeliveryStatusColor(message.deliveryStatus), - ), - const SizedBox(width: 3), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Text( - message.getLocalizedDeliveryStatus(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: getDeliveryStatusColor( - message.deliveryStatus, - ), - fontStyle: FontStyle.italic, + Builder( + builder: (context) { + final txEstimate = estimateMessageTransmitDuration( + message, + radioBw: connectionProvider.deviceInfo.radioBw, + radioSf: connectionProvider.deviceInfo.radioSf, + radioCr: connectionProvider.deviceInfo.radioCr, + ); + final showSentDirectStats = + message.isContactMessage && + message.deliveryStatus == + MessageDeliveryStatus.delivered && + _showReceivedStats && + message.roundTripTimeMs != null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon( + getDeliveryStatusIcon(message.deliveryStatus), + size: 12, + color: getDeliveryStatusColor( + message.deliveryStatus, ), - ), - ), - ), - // Show retry button for failed messages - if (message.deliveryStatus == - MessageDeliveryStatus.failed) ...[ - const SizedBox(width: 6), - GestureDetector( - onTap: () => _retryFailedMessage(context, message), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.orange.withValues(alpha: 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: 3), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text( + message.getLocalizedDeliveryStatus(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + color: getDeliveryStatusColor( + message.deliveryStatus, + ), + fontStyle: FontStyle.italic, + ), + ), ), - const SizedBox(width: 4), - Text( - 'Retry', - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: Colors.orange, - fontWeight: FontWeight.bold, + ), + // Show retry button for failed messages + if (message.deliveryStatus == + MessageDeliveryStatus.failed) ...[ + const SizedBox(width: 6), + GestureDetector( + onTap: () => + _retryFailedMessage(context, message), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.orange.withValues( + alpha: 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, + ), + ), + ], + ), + ), ), ], - ), + ], ), - ), - ], - ], + if (showSentDirectStats) ...[ + const SizedBox(height: 6), + buildSentDirectSignalStatus( + context, + message, + roundTripTimeMs: message.roundTripTimeMs!, + txEstimate: txEstimate, + ), + ], + ], + ); + }, ), if (shouldShowSentChannelStats( message, diff --git a/lib/widgets/messages/message_bubble_signal.dart b/lib/widgets/messages/message_bubble_signal.dart index 29abd54..572742f 100644 --- a/lib/widgets/messages/message_bubble_signal.dart +++ b/lib/widgets/messages/message_bubble_signal.dart @@ -179,6 +179,85 @@ Widget buildReceivedSignalStatus( ); } +Widget buildSentDirectSignalStatus( + BuildContext context, + Message message, { + required int roundTripTimeMs, + required Duration txEstimate, +}) { + final estimatedTransmitMs = sanitizeEstimatedTransmitMs( + estimatedTransmitMs: txEstimate > Duration.zero + ? txEstimate.inMilliseconds + : null, + senderToReceiptMs: roundTripTimeMs, + ); + final postTransmitDelayMs = estimatedTransmitMs != null + ? (roundTripTimeMs - estimatedTransmitMs).clamp(0, 86400000).toInt() + : null; + + return Wrap( + spacing: 4, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + _techChip( + context, + icon: Icons.alt_route, + label: hopDisplayLabel(message), + color: Colors.indigo, + ), + _techChip( + context, + icon: Icons.schedule, + label: _formatMs(roundTripTimeMs), + color: Colors.deepPurple, + ), + if (estimatedTransmitMs != null) + _techChip( + context, + icon: Icons.timelapse, + label: '~${_formatMs(estimatedTransmitMs)} tx', + color: Colors.blue, + ), + if (postTransmitDelayMs != null) + _techChip( + context, + icon: Icons.hourglass_bottom, + label: '+${_formatMs(postTransmitDelayMs)} lag', + color: Colors.orange, + ), + if (message.retryAttempt > 0) + _techChip( + context, + icon: Icons.refresh, + label: 'retry ${message.retryAttempt}/3', + color: Colors.redAccent, + ), + if (message.suggestedTimeoutMs != null) + _techChip( + context, + icon: Icons.timer_outlined, + label: 'timeout ${_formatMs(message.suggestedTimeoutMs!)}', + color: Colors.blueGrey, + ), + if (message.usedFloodFallback) + _techChip( + context, + icon: Icons.waves, + label: 'flood fallback', + color: Colors.teal, + ) + else if (message.expectedAckTag != null) + _techChip( + context, + icon: Icons.route, + label: 'direct ACK', + color: Colors.indigo, + ), + ], + ); +} + String _formatMs(int value) { if (value >= 60000) { final minutes = value ~/ 60000; diff --git a/lib/widgets/messages/voice_message_bubble.dart b/lib/widgets/messages/voice_message_bubble.dart index 45eff5a..715521d 100644 --- a/lib/widgets/messages/voice_message_bubble.dart +++ b/lib/widgets/messages/voice_message_bubble.dart @@ -374,6 +374,9 @@ class _VoiceMessageBubbleState extends State { ); try { + debugPrint( + 'đŸŽ™ī¸ [VoiceMessageBubble] Outgoing voice fetch request: session=$sessionId want=${isPartialResume ? 'missing' : 'all'} target=${sender.advName} hops=${sender.outPathLen}', + ); await connectionProvider.sendRawVoicePacket( contactPath: sender.outPath, contactPathLen: sender.outPathLen, diff --git a/pubspec.lock b/pubspec.lock index 42e7d53..692111d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -849,11 +849,9 @@ packages: meshcore_client: dependency: "direct main" description: - path: "." - ref: main - resolved-ref: "3f870e98ee9527a3137bfcbdd1454036912fb609" - url: "https://github.com/dz0ny/meshcore_client.git" - source: git + path: "../meshcore_client" + relative: true + source: path version: "0.1.0" meta: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index a5df551..99c8d7a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -148,6 +148,8 @@ dev_dependencies: dependency_overrides: path_provider_foundation: 2.5.1 + meshcore_client: + path: ../meshcore_client flutter_launcher_icons: android: "launcher_icon" diff --git a/test/models/message_reception_details_test.dart b/test/models/message_reception_details_test.dart index 98f17b7..2982281 100644 --- a/test/models/message_reception_details_test.dart +++ b/test/models/message_reception_details_test.dart @@ -2,6 +2,29 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:meshcore_sar_app/models/message_reception_details.dart'; void main() { + test('drops impossible transmit estimate for received messages', () { + expect( + sanitizeEstimatedTransmitMs( + estimatedTransmitMs: 16 * 60 * 1000 + 54 * 1000, + senderToReceiptMs: 4200, + ), + isNull, + ); + }); + + test( + 'keeps close transmit estimate despite second-level timestamp rounding', + () { + expect( + sanitizeEstimatedTransmitMs( + estimatedTransmitMs: 1800, + senderToReceiptMs: 900, + ), + 1800, + ); + }, + ); + test('round trips reception details json', () { final details = MessageReceptionDetails( capturedAt: DateTime.fromMillisecondsSinceEpoch(1700000000000), diff --git a/test/providers/helpers/raw_session_retransmit_test.dart b/test/providers/helpers/raw_session_retransmit_test.dart index 5f83aa1..512cd76 100644 --- a/test/providers/helpers/raw_session_retransmit_test.dart +++ b/test/providers/helpers/raw_session_retransmit_test.dart @@ -47,9 +47,8 @@ void main() { expect(ok, isFalse); }); - test('sends only requested indices and waits for ack', () async { + test('sends only requested indices', () async { final sent = []; - final waited = []; final ok = await serveCachedSessionFragments<_Fragment>( providerLabel: 'TestProvider', sessionId: 'deadbeef', @@ -70,15 +69,6 @@ void main() { }) async { sent.add(payload); }, - waitForFragmentAck: - ({ - required sessionId, - required index, - timeout = const Duration(seconds: 8), - }) async { - waited.add(index); - return true; - }, requestedIndices: {1, 2}, ); @@ -86,37 +76,6 @@ void main() { expect(sent.length, equals(2)); expect(sent[0], equals(Uint8List.fromList([20]))); expect(sent[1], equals(Uint8List.fromList([30]))); - expect(waited, equals([1, 2])); - }); - - test('fails when ack does not arrive', () async { - final ok = await serveCachedSessionFragments<_Fragment>( - providerLabel: 'TestProvider', - sessionId: 'deadbeef', - requester: _buildContact(outPathLen: 1), - fragments: [ - _Fragment(0, Uint8List.fromList([1])), - ], - maxDirectPayloadHops: 3, - indexOf: (f) => f.index, - encodeBinary: (f) => f.payload, - sendRawPacket: - ({ - required contactPath, - required contactPathLen, - required payload, - }) async {}, - waitForFragmentAck: - ({ - required sessionId, - required index, - timeout = const Duration(seconds: 8), - }) async { - return false; - }, - ); - - expect(ok, isFalse); }); test('fails when no requested index matches cached fragments', () async {