mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
Add nearest relay fallback toggle
This commit is contained in:
@@ -98,6 +98,9 @@ class AppProvider with ChangeNotifier {
|
|||||||
bool _clearPathOnMaxRetry =
|
bool _clearPathOnMaxRetry =
|
||||||
MessagingRoutePreferences.defaultClearPathOnMaxRetry;
|
MessagingRoutePreferences.defaultClearPathOnMaxRetry;
|
||||||
bool get clearPathOnMaxRetry => _clearPathOnMaxRetry;
|
bool get clearPathOnMaxRetry => _clearPathOnMaxRetry;
|
||||||
|
bool _nearestRelayFallbackEnabled =
|
||||||
|
MessagingRoutePreferences.defaultNearestRelayFallbackEnabled;
|
||||||
|
bool get nearestRelayFallbackEnabled => _nearestRelayFallbackEnabled;
|
||||||
final PathHistoryService _pathHistoryService = PathHistoryService();
|
final PathHistoryService _pathHistoryService = PathHistoryService();
|
||||||
final NearestRouterSelector _nearestRouterSelector =
|
final NearestRouterSelector _nearestRouterSelector =
|
||||||
const NearestRouterSelector();
|
const NearestRouterSelector();
|
||||||
@@ -458,6 +461,8 @@ class AppProvider with ChangeNotifier {
|
|||||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled();
|
await MessagingRoutePreferences.getAutoRouteRotationEnabled();
|
||||||
_clearPathOnMaxRetry =
|
_clearPathOnMaxRetry =
|
||||||
await MessagingRoutePreferences.getClearPathOnMaxRetry();
|
await MessagingRoutePreferences.getClearPathOnMaxRetry();
|
||||||
|
_nearestRelayFallbackEnabled =
|
||||||
|
await MessagingRoutePreferences.getNearestRelayFallbackEnabled();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading messaging route settings: $e');
|
debugPrint('Error loading messaging route settings: $e');
|
||||||
@@ -484,6 +489,16 @@ class AppProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> toggleNearestRelayFallbackEnabled(bool enabled) async {
|
||||||
|
try {
|
||||||
|
_nearestRelayFallbackEnabled = enabled;
|
||||||
|
await MessagingRoutePreferences.setNearestRelayFallbackEnabled(enabled);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error saving nearest relay fallback setting: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize location tracking service
|
/// Initialize location tracking service
|
||||||
Future<void> _initializeLocationTracking() async {
|
Future<void> _initializeLocationTracking() async {
|
||||||
try {
|
try {
|
||||||
@@ -713,6 +728,19 @@ class AppProvider with ChangeNotifier {
|
|||||||
final receptionDetailsSnapshot = _buildReceptionDetailsSnapshot(
|
final receptionDetailsSnapshot = _buildReceptionDetailsSnapshot(
|
||||||
enrichedMessage,
|
enrichedMessage,
|
||||||
);
|
);
|
||||||
|
final receivedPathBytes = receptionDetailsSnapshot?.pathBytes;
|
||||||
|
if (senderContact != null &&
|
||||||
|
enrichedMessage.isChannelMessage &&
|
||||||
|
(enrichedMessage.channelIdx ?? 0) == 0 &&
|
||||||
|
receivedPathBytes != null &&
|
||||||
|
receivedPathBytes.isNotEmpty) {
|
||||||
|
unawaited(
|
||||||
|
_learnPathFromPublicMessage(
|
||||||
|
contact: senderContact,
|
||||||
|
pathBytes: receivedPathBytes,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if message is a drawing broadcast
|
// Check if message is a drawing broadcast
|
||||||
if (DrawingMessageParser.isDrawingMessage(enrichedMessage.text)) {
|
if (DrawingMessageParser.isDrawingMessage(enrichedMessage.text)) {
|
||||||
@@ -1379,6 +1407,10 @@ class AppProvider with ChangeNotifier {
|
|||||||
required Contact contact,
|
required Contact contact,
|
||||||
required Message message,
|
required Message message,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (!_nearestRelayFallbackEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final latestContact =
|
final latestContact =
|
||||||
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
||||||
final session =
|
final session =
|
||||||
@@ -1491,6 +1523,38 @@ class AppProvider with ChangeNotifier {
|
|||||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _learnPathFromPublicMessage({
|
||||||
|
required Contact contact,
|
||||||
|
required List<int> pathBytes,
|
||||||
|
}) async {
|
||||||
|
final preferred = await RouteHashPreferences.getHashSize();
|
||||||
|
final inferredHashSize = _inferReceivedPathHashSize(
|
||||||
|
pathBytes,
|
||||||
|
preferredHashSize: preferred,
|
||||||
|
);
|
||||||
|
await _pathHistoryService.recordReceivedBytePath(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
pathBytes,
|
||||||
|
inferredHashSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _inferReceivedPathHashSize(
|
||||||
|
List<int> pathBytes, {
|
||||||
|
required int preferredHashSize,
|
||||||
|
}) {
|
||||||
|
final preferred = preferredHashSize;
|
||||||
|
final candidates = {preferred, 3, 2, 1}.toList();
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (candidate >= 1 &&
|
||||||
|
candidate <= 3 &&
|
||||||
|
pathBytes.length % candidate == 0) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize the app (load contacts, sync time, etc.)
|
/// Initialize the app (load contacts, sync time, etc.)
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (!connectionProvider.deviceInfo.isConnected) return;
|
if (!connectionProvider.deviceInfo.isConnected) return;
|
||||||
|
|||||||
@@ -988,6 +988,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Consumer<AppProvider>(
|
||||||
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.route),
|
||||||
|
title: const Text('Nearest repeater fallback'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'After normal retries fail, try one final resend through the nearest repeater',
|
||||||
|
),
|
||||||
|
value: appProvider.nearestRelayFallbackEnabled,
|
||||||
|
onChanged: (value) async {
|
||||||
|
await appProvider.toggleNearestRelayFallbackEnabled(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
Consumer<AppProvider>(
|
Consumer<AppProvider>(
|
||||||
builder: (context, appProvider, child) => SwitchListTile(
|
builder: (context, appProvider, child) => SwitchListTile(
|
||||||
secondary: const Icon(Icons.route),
|
secondary: const Icon(Icons.route),
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
class MessagingRoutePreferences {
|
class MessagingRoutePreferences {
|
||||||
static const bool defaultAutoRouteRotationEnabled = false;
|
static const bool defaultAutoRouteRotationEnabled = false;
|
||||||
static const bool defaultClearPathOnMaxRetry = false;
|
static const bool defaultClearPathOnMaxRetry = false;
|
||||||
|
static const bool defaultNearestRelayFallbackEnabled = true;
|
||||||
|
|
||||||
static const String _autoRouteRotationKey =
|
static const String _autoRouteRotationKey =
|
||||||
'messaging_auto_route_rotation_enabled';
|
'messaging_auto_route_rotation_enabled';
|
||||||
static const String _clearPathOnMaxRetryKey =
|
static const String _clearPathOnMaxRetryKey =
|
||||||
'messaging_clear_path_on_max_retry';
|
'messaging_clear_path_on_max_retry';
|
||||||
|
static const String _nearestRelayFallbackKey =
|
||||||
|
'messaging_nearest_relay_fallback_enabled';
|
||||||
|
|
||||||
static Future<bool> getAutoRouteRotationEnabled() async {
|
static Future<bool> getAutoRouteRotationEnabled() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -29,4 +32,15 @@ class MessagingRoutePreferences {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_clearPathOnMaxRetryKey, enabled);
|
await prefs.setBool(_clearPathOnMaxRetryKey, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<bool> getNearestRelayFallbackEnabled() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getBool(_nearestRelayFallbackKey) ??
|
||||||
|
defaultNearestRelayFallbackEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> setNearestRelayFallbackEnabled(bool enabled) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_nearestRelayFallbackKey, enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,45 @@ class PathHistoryService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> recordReceivedBytePath(
|
||||||
|
String contactPublicKeyHex,
|
||||||
|
List<int> pathBytes,
|
||||||
|
int hashSize,
|
||||||
|
) async {
|
||||||
|
await initialize();
|
||||||
|
if (pathBytes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hashSize < 1 || hashSize > 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathBytes.length % hashSize != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final history = _historyFor(contactPublicKeyHex);
|
||||||
|
final signature = pathBytes
|
||||||
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
final existing = _findDirectPath(history.directPaths, signature);
|
||||||
|
final updated = PathRecord(
|
||||||
|
pathBytes: List<int>.from(pathBytes),
|
||||||
|
hopCount: pathBytes.length ~/ hashSize,
|
||||||
|
hashSize: hashSize,
|
||||||
|
successCount: existing?.successCount ?? 0,
|
||||||
|
failureCount: existing?.failureCount ?? 0,
|
||||||
|
lastRoundTripTimeMs: existing?.lastRoundTripTimeMs ?? 0,
|
||||||
|
lastUsedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _saveHistory(
|
||||||
|
contactPublicKeyHex,
|
||||||
|
history.copyWith(
|
||||||
|
directPaths: _upsertDirectPath(history.directPaths, updated),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<PathSelection> getSelectionForContact(
|
Future<PathSelection> getSelectionForContact(
|
||||||
Contact contact, {
|
Contact contact, {
|
||||||
required bool autoRouteRotationEnabled,
|
required bool autoRouteRotationEnabled,
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
|
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
|
||||||
|
nearestRelayFallbackEnabled:
|
||||||
|
appProvider.nearestRelayFallbackEnabled,
|
||||||
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
|
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -264,12 +266,14 @@ class _AutomationRoutingInfo extends StatelessWidget {
|
|||||||
final bool isExpanded;
|
final bool isExpanded;
|
||||||
final VoidCallback onToggle;
|
final VoidCallback onToggle;
|
||||||
final bool autoRouteRotationEnabled;
|
final bool autoRouteRotationEnabled;
|
||||||
|
final bool nearestRelayFallbackEnabled;
|
||||||
final bool clearPathOnMaxRetry;
|
final bool clearPathOnMaxRetry;
|
||||||
|
|
||||||
const _AutomationRoutingInfo({
|
const _AutomationRoutingInfo({
|
||||||
required this.isExpanded,
|
required this.isExpanded,
|
||||||
required this.onToggle,
|
required this.onToggle,
|
||||||
required this.autoRouteRotationEnabled,
|
required this.autoRouteRotationEnabled,
|
||||||
|
required this.nearestRelayFallbackEnabled,
|
||||||
required this.clearPathOnMaxRetry,
|
required this.clearPathOnMaxRetry,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,6 +340,12 @@ class _AutomationRoutingInfo extends StatelessWidget {
|
|||||||
: 'Auto route rotation off',
|
: 'Auto route rotation off',
|
||||||
icon: Icons.swap_horiz,
|
icon: Icons.swap_horiz,
|
||||||
),
|
),
|
||||||
|
_InfoChip(
|
||||||
|
label: nearestRelayFallbackEnabled
|
||||||
|
? 'Nearest repeater fallback on'
|
||||||
|
: 'Nearest repeater fallback off',
|
||||||
|
icon: Icons.router,
|
||||||
|
),
|
||||||
_InfoChip(
|
_InfoChip(
|
||||||
label: clearPathOnMaxRetry
|
label: clearPathOnMaxRetry
|
||||||
? 'Clear path on max retry on'
|
? 'Clear path on max retry on'
|
||||||
|
|||||||
@@ -16,16 +16,25 @@ void main() {
|
|||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isFalse);
|
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isFalse);
|
||||||
|
expect(
|
||||||
|
await MessagingRoutePreferences.getNearestRelayFallbackEnabled(),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('route preferences persist changes', () async {
|
test('route preferences persist changes', () async {
|
||||||
await MessagingRoutePreferences.setAutoRouteRotationEnabled(true);
|
await MessagingRoutePreferences.setAutoRouteRotationEnabled(true);
|
||||||
await MessagingRoutePreferences.setClearPathOnMaxRetry(true);
|
await MessagingRoutePreferences.setClearPathOnMaxRetry(true);
|
||||||
|
await MessagingRoutePreferences.setNearestRelayFallbackEnabled(false);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
|
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isTrue);
|
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isTrue);
|
||||||
|
expect(
|
||||||
|
await MessagingRoutePreferences.getNearestRelayFallbackEnabled(),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,4 +127,16 @@ void main() {
|
|||||||
|
|
||||||
expect(selection.mode, PathSelectionMode.flood);
|
expect(selection.mode, PathSelectionMode.flood);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('received public byte path is added to history', () async {
|
||||||
|
final service = PathHistoryService();
|
||||||
|
await service.initialize();
|
||||||
|
await service.recordReceivedBytePath('abc123', [0x01, 0x02, 0x03], 3);
|
||||||
|
|
||||||
|
final history = service.historyFor('abc123');
|
||||||
|
expect(history.directPaths, hasLength(1));
|
||||||
|
expect(history.directPaths.single.pathBytes, [0x01, 0x02, 0x03]);
|
||||||
|
expect(history.directPaths.single.hashSize, 3);
|
||||||
|
expect(history.directPaths.single.hopCount, 1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user