mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Fix null check crash in TabBar
This commit is contained in:
@@ -42,6 +42,8 @@ class AppProvider with ChangeNotifier {
|
||||
|
||||
bool _isMapEnabled = true;
|
||||
bool get isMapEnabled => _isMapEnabled;
|
||||
bool _isContactsEnabled = true;
|
||||
bool get isContactsEnabled => _isContactsEnabled;
|
||||
|
||||
bool _isVoiceSilenceTrimmingEnabled = true;
|
||||
bool get isVoiceSilenceTrimmingEnabled => _isVoiceSilenceTrimmingEnabled;
|
||||
@@ -81,6 +83,7 @@ class AppProvider with ChangeNotifier {
|
||||
_initializeLocationTracking();
|
||||
_loadSimpleMode();
|
||||
_loadMapEnabled();
|
||||
_loadContactsEnabled();
|
||||
_loadVoiceSilenceTrimmingEnabled();
|
||||
_loadVoiceBandPassFilterEnabled();
|
||||
_loadVoiceCompressorEnabled();
|
||||
@@ -214,6 +217,29 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load contacts enabled setting from shared preferences
|
||||
Future<void> _loadContactsEnabled() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_isContactsEnabled = prefs.getBool('contacts_enabled') ?? true;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error loading contacts enabled setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle contacts tab on/off
|
||||
Future<void> toggleContactsEnabled(bool enabled) async {
|
||||
try {
|
||||
_isContactsEnabled = enabled;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('contacts_enabled', enabled);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving contacts enabled setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Load voice silence trimming setting from shared preferences.
|
||||
Future<void> _loadVoiceSilenceTrimmingEnabled() async {
|
||||
try {
|
||||
@@ -407,24 +433,26 @@ 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,
|
||||
);
|
||||
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) {
|
||||
|
||||
@@ -65,8 +65,8 @@ class ConnectionProvider with ChangeNotifier {
|
||||
/// Active service — BLE or TCP depending on current mode
|
||||
MeshCoreServiceBase get _activeService =>
|
||||
(_connectionMode == ConnectionMode.tcp && _tcpService != null)
|
||||
? _tcpService!
|
||||
: _bleService;
|
||||
? _tcpService!
|
||||
: _bleService;
|
||||
|
||||
/// Current connection mode
|
||||
ConnectionMode _connectionMode = ConnectionMode.ble;
|
||||
@@ -117,6 +117,8 @@ class ConnectionProvider with ChangeNotifier {
|
||||
bool _noMoreMessages = false;
|
||||
// Prevent overlapping/too-frequent sync requests
|
||||
bool _isSyncingMessages = false;
|
||||
// If MSG_WAITING arrives while a sync loop is active, queue one more pass.
|
||||
bool _syncRequestedWhileBusy = false;
|
||||
DateTime? _lastSyncNextRequestedAt;
|
||||
static const Duration _minSyncNextInterval = Duration(milliseconds: 150);
|
||||
|
||||
@@ -310,7 +312,14 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
service.onMessageWaiting = () {
|
||||
debugPrint('📥 [Provider] MSG_WAITING - auto-syncing');
|
||||
syncAllMessages();
|
||||
if (_isSyncingMessages) {
|
||||
_syncRequestedWhileBusy = true;
|
||||
debugPrint(
|
||||
' ↪️ [Provider] Sync already running; queued follow-up sync',
|
||||
);
|
||||
return;
|
||||
}
|
||||
unawaited(syncAllMessages());
|
||||
};
|
||||
|
||||
service.onLoginSuccess =
|
||||
@@ -558,7 +567,9 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
final success = await _tcpService!.connect(host, port);
|
||||
if (!success) {
|
||||
_deviceInfo = _deviceInfo.copyWith(connectionState: ConnectionState.error);
|
||||
_deviceInfo = _deviceInfo.copyWith(
|
||||
connectionState: ConnectionState.error,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
return success;
|
||||
@@ -1169,7 +1180,10 @@ class ConnectionProvider with ChangeNotifier {
|
||||
debugPrint(' Text: $text');
|
||||
debugPrint(' MessageID: $messageId');
|
||||
|
||||
await _activeService.sendChannelMessage(channelIdx: channelIdx, text: text);
|
||||
await _activeService.sendChannelMessage(
|
||||
channelIdx: channelIdx,
|
||||
text: text,
|
||||
);
|
||||
|
||||
debugPrint('✅ [ConnectionProvider] BLE send completed');
|
||||
debugPrint(
|
||||
@@ -1655,6 +1669,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
Future<int> syncAllMessages() async {
|
||||
if (_isSyncingMessages) {
|
||||
// Already syncing; avoid overlapping loops
|
||||
_syncRequestedWhileBusy = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1664,83 +1679,94 @@ class ConnectionProvider with ChangeNotifier {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
_noMoreMessages = false; // Reset flag
|
||||
int totalCount = 0;
|
||||
|
||||
try {
|
||||
_isSyncingMessages = true;
|
||||
debugPrint('🔄 [Provider] Starting message sync loop...');
|
||||
debugPrint(' Initial _noMoreMessages state: $_noMoreMessages');
|
||||
do {
|
||||
_syncRequestedWhileBusy = false;
|
||||
_noMoreMessages = false; // Reset flag per pass
|
||||
int passCount = 0;
|
||||
debugPrint('🔄 [Provider] Starting message sync loop...');
|
||||
debugPrint(' Initial _noMoreMessages state: $_noMoreMessages');
|
||||
|
||||
// Keep syncing until we get NoMoreMessages response
|
||||
// The device will send ContactMsgRecv or ChannelMsgRecv responses
|
||||
// until it sends NoMoreMessages
|
||||
for (int i = 0; i < 100; i++) {
|
||||
// Safety limit
|
||||
// Check flag BEFORE sending (not after)
|
||||
if (_noMoreMessages) {
|
||||
debugPrint(
|
||||
'✅ [Provider] Message sync complete - NoMoreMessages flag set after $passCount requests',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Keep syncing until we get NoMoreMessages response
|
||||
// The device will send ContactMsgRecv or ChannelMsgRecv responses
|
||||
// until it sends NoMoreMessages
|
||||
for (int i = 0; i < 100; i++) {
|
||||
// Safety limit
|
||||
// Check flag BEFORE sending (not after)
|
||||
if (_noMoreMessages) {
|
||||
debugPrint(
|
||||
'✅ [Provider] Message sync complete - NoMoreMessages flag set after $count requests',
|
||||
'📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'📤 [Provider] Sync iteration ${i + 1}: Sending CMD_SYNC_NEXT_MESSAGE',
|
||||
);
|
||||
// Create new completer for this request
|
||||
_syncResponseCompleter = Completer<bool>();
|
||||
|
||||
// Create new completer for this request
|
||||
_syncResponseCompleter = Completer<bool>();
|
||||
// Respect the minimum interval between requests
|
||||
final now = DateTime.now();
|
||||
if (_lastSyncNextRequestedAt != null) {
|
||||
final elapsed = now.difference(_lastSyncNextRequestedAt!);
|
||||
if (elapsed < _minSyncNextInterval) {
|
||||
final remaining = _minSyncNextInterval - elapsed;
|
||||
await Future.delayed(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
// Respect the minimum interval between requests
|
||||
final now = DateTime.now();
|
||||
if (_lastSyncNextRequestedAt != null) {
|
||||
final elapsed = now.difference(_lastSyncNextRequestedAt!);
|
||||
if (elapsed < _minSyncNextInterval) {
|
||||
final remaining = _minSyncNextInterval - elapsed;
|
||||
await Future.delayed(remaining);
|
||||
await _activeService.syncNextMessage();
|
||||
_lastSyncNextRequestedAt = DateTime.now();
|
||||
passCount++;
|
||||
totalCount++;
|
||||
|
||||
// Wait for response (true = message received, false = no more messages)
|
||||
// Timeout after 2 seconds to prevent hanging
|
||||
final hasMore = await _syncResponseCompleter!.future.timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () {
|
||||
debugPrint('⚠️ [Provider] Sync timeout - no response after 2s');
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
' After iteration ${i + 1}: hasMore=$hasMore, _noMoreMessages=$_noMoreMessages',
|
||||
);
|
||||
|
||||
if (!hasMore) {
|
||||
debugPrint(' ✅ No more messages available, stopping sync');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await _activeService.syncNextMessage();
|
||||
_lastSyncNextRequestedAt = DateTime.now();
|
||||
count++;
|
||||
|
||||
// Wait for response (true = message received, false = no more messages)
|
||||
// Timeout after 2 seconds to prevent hanging
|
||||
final hasMore = await _syncResponseCompleter!.future.timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () {
|
||||
debugPrint('⚠️ [Provider] Sync timeout - no response after 2s');
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
' After iteration ${i + 1}: hasMore=$hasMore, _noMoreMessages=$_noMoreMessages',
|
||||
);
|
||||
|
||||
if (!hasMore) {
|
||||
debugPrint(' ✅ No more messages available, stopping sync');
|
||||
break;
|
||||
if (!_noMoreMessages && passCount >= 100) {
|
||||
debugPrint(
|
||||
'⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_noMoreMessages && count >= 100) {
|
||||
debugPrint(
|
||||
'⚠️ [Provider] Message sync stopped - reached safety limit of 100 requests without NoMoreMessages',
|
||||
);
|
||||
}
|
||||
if (_syncRequestedWhileBusy) {
|
||||
debugPrint(
|
||||
'↻ [Provider] MSG_WAITING received during sync; running another pass',
|
||||
);
|
||||
}
|
||||
} while (_syncRequestedWhileBusy && _activeService.isConnected);
|
||||
|
||||
debugPrint(
|
||||
'🏁 [Provider] Message sync finished: sent $count sync requests, _noMoreMessages=$_noMoreMessages',
|
||||
'🏁 [Provider] Message sync finished: sent $totalCount sync requests, _noMoreMessages=$_noMoreMessages',
|
||||
);
|
||||
return count;
|
||||
return totalCount;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Provider] Failed to sync messages: $e');
|
||||
_error = 'Failed to sync messages: $e';
|
||||
notifyListeners();
|
||||
return count;
|
||||
return totalCount;
|
||||
} finally {
|
||||
_isSyncingMessages = false;
|
||||
_syncResponseCompleter = null;
|
||||
|
||||
102
lib/providers/helpers/raw_session_retransmit.dart
Normal file
102
lib/providers/helpers/raw_session_retransmit.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../models/contact.dart';
|
||||
|
||||
typedef RawPacketSender =
|
||||
Future<void> Function({
|
||||
required Uint8List contactPath,
|
||||
required int contactPathLen,
|
||||
required Uint8List payload,
|
||||
});
|
||||
|
||||
typedef FragmentAckWaiter =
|
||||
Future<bool> Function({
|
||||
required String sessionId,
|
||||
required int index,
|
||||
Duration timeout,
|
||||
});
|
||||
|
||||
Future<bool> serveCachedSessionFragments<T>({
|
||||
required String providerLabel,
|
||||
required String sessionId,
|
||||
required Contact requester,
|
||||
required List<T> fragments,
|
||||
required int maxDirectPayloadHops,
|
||||
required int Function(T fragment) indexOf,
|
||||
required Uint8List Function(T fragment) encodeBinary,
|
||||
required RawPacketSender? sendRawPacket,
|
||||
FragmentAckWaiter? waitForFragmentAck,
|
||||
Set<int>? requestedIndices,
|
||||
Duration ackTimeout = const Duration(seconds: 8),
|
||||
}) async {
|
||||
if (fragments.isEmpty) {
|
||||
debugPrint('⚠️ [$providerLabel] No cached fragments for $sessionId');
|
||||
return false;
|
||||
}
|
||||
if (sendRawPacket == null) {
|
||||
debugPrint('⚠️ [$providerLabel] sendRawPacketCallback not set');
|
||||
return false;
|
||||
}
|
||||
if (requester.outPathLen < 0) {
|
||||
debugPrint('⚠️ [$providerLabel] ${requester.advName} has no direct path');
|
||||
return false;
|
||||
}
|
||||
if (requester.outPathLen > maxDirectPayloadHops) {
|
||||
debugPrint(
|
||||
'⚠️ [$providerLabel] ${requester.advName} is too far: ${requester.outPathLen} hops (max $maxDirectPayloadHops)',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (requester.outPath.isEmpty) {
|
||||
debugPrint(
|
||||
'⚠️ [$providerLabel] ${requester.advName} has empty outPath payload',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
var servedCount = 0;
|
||||
for (final fragment in fragments) {
|
||||
final index = indexOf(fragment);
|
||||
if (index < 0) {
|
||||
debugPrint('⚠️ [$providerLabel] Invalid fragment index $index');
|
||||
continue;
|
||||
}
|
||||
if (requestedIndices != null && !requestedIndices.contains(index)) {
|
||||
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',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (servedCount == 0) {
|
||||
debugPrint(
|
||||
'⚠️ [$providerLabel] No fragments matched request for $sessionId',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
debugPrint('✅ [$providerLabel] Served $servedCount fragments for $sessionId');
|
||||
return true;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
|
||||
/// Reassembly state for one incoming image session.
|
||||
@@ -137,19 +138,8 @@ class ImageProvider with ChangeNotifier {
|
||||
/// Register envelope metadata for a session (called when IE1 is received
|
||||
/// before any binary fragments arrive).
|
||||
void registerEnvelope(ImageEnvelope envelope) {
|
||||
_sessions.putIfAbsent(
|
||||
envelope.sessionId,
|
||||
() => ImageSession(
|
||||
sessionId: envelope.sessionId,
|
||||
format: envelope.format,
|
||||
total: envelope.total,
|
||||
width: envelope.width,
|
||||
height: envelope.height,
|
||||
),
|
||||
);
|
||||
// Update dimensions if we created the session from a fragment (w/h = 0).
|
||||
final session = _sessions[envelope.sessionId]!;
|
||||
if (session.width == 0 || session.height == 0) {
|
||||
final existing = _sessions[envelope.sessionId];
|
||||
if (existing == null) {
|
||||
_sessions[envelope.sessionId] = ImageSession(
|
||||
sessionId: envelope.sessionId,
|
||||
format: envelope.format,
|
||||
@@ -157,12 +147,38 @@ class ImageProvider with ChangeNotifier {
|
||||
width: envelope.width,
|
||||
height: envelope.height,
|
||||
);
|
||||
// Copy existing fragments into the new session.
|
||||
final old = _sessions[envelope.sessionId]!;
|
||||
for (var i = 0; i < session.fragments.length && i < old.total; i++) {
|
||||
old.fragments[i] = session.fragments[i];
|
||||
unawaited(_persist());
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final needsMerge =
|
||||
existing.width == 0 ||
|
||||
existing.height == 0 ||
|
||||
existing.total != envelope.total ||
|
||||
existing.format != envelope.format;
|
||||
if (!needsMerge) {
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
final merged = ImageSession(
|
||||
sessionId: envelope.sessionId,
|
||||
format: envelope.format,
|
||||
total: envelope.total,
|
||||
width: envelope.width,
|
||||
height: envelope.height,
|
||||
);
|
||||
merged.firstFragmentAt = existing.firstFragmentAt;
|
||||
merged.lastFragmentAt = existing.lastFragmentAt;
|
||||
for (final fragment in existing.fragments) {
|
||||
if (fragment == null) continue;
|
||||
if (fragment.index < merged.total) {
|
||||
merged.fragments[fragment.index] = fragment;
|
||||
}
|
||||
}
|
||||
_sessions[envelope.sessionId] = merged;
|
||||
unawaited(_persist());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -214,55 +230,18 @@ class ImageProvider with ChangeNotifier {
|
||||
debugPrint('⚠️ [ImageProvider] No cached session for $sessionId');
|
||||
return false;
|
||||
}
|
||||
if (sendRawPacketCallback == null) {
|
||||
debugPrint('⚠️ [ImageProvider] sendRawPacketCallback not set');
|
||||
return false;
|
||||
}
|
||||
if (requester.outPathLen < 0) {
|
||||
debugPrint('⚠️ [ImageProvider] ${requester.advName} has no direct path');
|
||||
return false;
|
||||
}
|
||||
if (requester.outPathLen > maxDirectPayloadHops) {
|
||||
debugPrint(
|
||||
'⚠️ [ImageProvider] ${requester.advName} is too far: ${requester.outPathLen} hops (max $maxDirectPayloadHops)',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final fragment in cached.fragments) {
|
||||
if (requestedIndices != null &&
|
||||
!requestedIndices.contains(fragment.index)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final ackFuture = waitForFragmentAckCallback?.call(
|
||||
sessionId: sessionId,
|
||||
index: fragment.index,
|
||||
timeout: const Duration(seconds: 8),
|
||||
);
|
||||
await sendRawPacketCallback!(
|
||||
contactPath: requester.outPath,
|
||||
contactPathLen: requester.outPathLen,
|
||||
payload: fragment.encodeBinary(),
|
||||
);
|
||||
if (ackFuture != null) {
|
||||
final acked = await ackFuture;
|
||||
if (!acked) {
|
||||
debugPrint(
|
||||
'⚠️ [ImageProvider] ACK timeout for $sessionId#${fragment.index}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('❌ [ImageProvider] Serve error for $sessionId: $e\n$st');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
debugPrint(
|
||||
'📷 [ImageProvider] Served ${cached.fragments.length} fragments of $sessionId',
|
||||
return serveCachedSessionFragments<ImagePacket>(
|
||||
providerLabel: 'ImageProvider',
|
||||
sessionId: sessionId,
|
||||
requester: requester,
|
||||
fragments: cached.fragments,
|
||||
maxDirectPayloadHops: maxDirectPayloadHops,
|
||||
indexOf: (fragment) => fragment.index,
|
||||
encodeBinary: (fragment) => fragment.encodeBinary(),
|
||||
sendRawPacket: sendRawPacketCallback,
|
||||
waitForFragmentAck: waitForFragmentAckCallback,
|
||||
requestedIndices: requestedIndices,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Persistence ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'helpers/raw_session_retransmit.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
import '../services/voice_codec_service.dart';
|
||||
import '../services/voice_player_service.dart';
|
||||
@@ -181,56 +182,18 @@ class VoiceProvider with ChangeNotifier {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (sendRawPacketCallback == null) {
|
||||
debugPrint('⚠️ [VoiceProvider] sendRawPacketCallback is not set');
|
||||
return false;
|
||||
}
|
||||
if (requester.outPathLen < 0) {
|
||||
debugPrint(
|
||||
'⚠️ [VoiceProvider] Requester ${requester.advName} has no direct path',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (requester.outPathLen > maxDirectPayloadHops) {
|
||||
debugPrint(
|
||||
'⚠️ [VoiceProvider] Requester ${requester.advName} is too far: ${requester.outPathLen} hops (max $maxDirectPayloadHops)',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final packet in cached.packets) {
|
||||
if (requestedIndices != null &&
|
||||
!requestedIndices.contains(packet.index)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final ackFuture = waitForFragmentAckCallback?.call(
|
||||
sessionId: sessionId,
|
||||
index: packet.index,
|
||||
timeout: const Duration(seconds: 8),
|
||||
);
|
||||
await sendRawPacketCallback!(
|
||||
contactPath: requester.outPath,
|
||||
contactPathLen: requester.outPathLen,
|
||||
payload: packet.encodeBinary(),
|
||||
);
|
||||
if (ackFuture != null) {
|
||||
final acked = await ackFuture;
|
||||
if (!acked) {
|
||||
debugPrint(
|
||||
'⚠️ [VoiceProvider] ACK timeout for $sessionId#${packet.index}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint(
|
||||
'❌ [VoiceProvider] Failed serving packet for $sessionId: $e\n$st',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return serveCachedSessionFragments<VoicePacket>(
|
||||
providerLabel: 'VoiceProvider',
|
||||
sessionId: sessionId,
|
||||
requester: requester,
|
||||
fragments: cached.packets,
|
||||
maxDirectPayloadHops: maxDirectPayloadHops,
|
||||
indexOf: (packet) => packet.index,
|
||||
encodeBinary: (packet) => packet.encodeBinary(),
|
||||
sendRawPacket: sendRawPacketCallback,
|
||||
waitForFragmentAck: waitForFragmentAckCallback,
|
||||
requestedIndices: requestedIndices,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Playback ─────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user