mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
Merge branch 'main' into fix/linux-ble-pairing-flow
This commit is contained in:
@@ -64,6 +64,10 @@ class AppSettingsService extends ChangeNotifier {
|
||||
await updateSettings(_settings.copyWith(mapShowOtherNodes: value));
|
||||
}
|
||||
|
||||
Future<void> setMapShowOverlaps(bool value) async {
|
||||
await updateSettings(_settings.copyWith(mapShowOverlaps: value));
|
||||
}
|
||||
|
||||
Future<void> setMapTimeFilterHours(double value) async {
|
||||
await updateSettings(_settings.copyWith(mapTimeFilterHours: value));
|
||||
}
|
||||
@@ -214,4 +218,8 @@ class AppSettingsService extends ChangeNotifier {
|
||||
Future<void> setTcpServerPort(int value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerPort: value));
|
||||
}
|
||||
|
||||
Future<void> setJumpToOldestUnread(bool value) async {
|
||||
await updateSettings(_settings.copyWith(jumpToOldestUnread: value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'app_debug_log_service.dart';
|
||||
|
||||
class _AckHistoryEntry {
|
||||
final String messageId;
|
||||
final List<Uint8List> ackHashes;
|
||||
final List<int> ackHashes;
|
||||
final DateTime timestamp;
|
||||
|
||||
_AckHistoryEntry({
|
||||
@@ -21,11 +21,16 @@ class _AckHistoryEntry {
|
||||
});
|
||||
}
|
||||
|
||||
/// (messageId, timestamp, attemptIndex) — stored per ACK hash for O(1) lookup.
|
||||
/// (messageId, timestamp, attemptIndex, pathSelection) — stored per ACK hash
|
||||
/// for O(1) lookup. [pathSelection] snapshots the route used for this
|
||||
/// specific attempt so that a late PUSH_CODE_SEND_CONFIRMED credits the
|
||||
/// correct path even when the message has since been retried on a different
|
||||
/// route.
|
||||
typedef AckHashMapping = ({
|
||||
String messageId,
|
||||
DateTime timestamp,
|
||||
int attemptIndex,
|
||||
PathSelection? pathSelection,
|
||||
});
|
||||
|
||||
class RetryServiceConfig {
|
||||
@@ -77,7 +82,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
final Map<String, Contact> _pendingContacts = {};
|
||||
final Map<String, List<PathSelection>> _attemptPathHistory = {};
|
||||
final Map<String, AckHashMapping> _ackHashToMessageId = {};
|
||||
final Map<String, List<Uint8List>> _expectedAckHashes = {};
|
||||
final Map<String, List<int>> _expectedAckHashes = {};
|
||||
final List<_AckHistoryEntry> _ackHistory = [];
|
||||
final Map<String, List<String>> _sendQueue = {};
|
||||
final Set<String> _activeMessages = {};
|
||||
@@ -98,7 +103,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
|
||||
/// Compute expected ACK hash using same algorithm as firmware:
|
||||
/// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes
|
||||
static Uint8List computeExpectedAckHash(
|
||||
static int computeExpectedAckHash(
|
||||
int timestampSeconds,
|
||||
int attempt,
|
||||
String text,
|
||||
@@ -126,7 +131,8 @@ class MessageRetryService extends ChangeNotifier {
|
||||
|
||||
// Compute SHA256 and return first 4 bytes
|
||||
final hash = sha256.convert(buffer);
|
||||
return Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
|
||||
}
|
||||
|
||||
Future<void> sendMessageWithRetry({
|
||||
@@ -324,9 +330,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
outboundText,
|
||||
selfPubKey,
|
||||
);
|
||||
final expectedHashHex = expectedHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final expectedHashHex = expectedHash.toRadixString(16).padLeft(8, '0');
|
||||
_expectedHashToMessageId[expectedHashHex] = messageId;
|
||||
|
||||
final shortText = message.text.length > 20
|
||||
@@ -341,13 +345,11 @@ class MessageRetryService extends ChangeNotifier {
|
||||
config.sendMessage(contact, message.text, attempt, timestampSeconds);
|
||||
}
|
||||
|
||||
bool updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
||||
bool updateMessageFromSent(int ackHash, int timeoutMs) {
|
||||
final config = _config;
|
||||
if (config == null) return false;
|
||||
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||
|
||||
// Try hash-based matching (fixes LoRa message drops causing mismatches)
|
||||
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
|
||||
@@ -385,14 +387,13 @@ class MessageRetryService extends ChangeNotifier {
|
||||
messageId: messageId,
|
||||
timestamp: DateTime.now(),
|
||||
attemptIndex: message.retryCount,
|
||||
pathSelection: _selectionFromMessage(message),
|
||||
);
|
||||
|
||||
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
||||
_expectedAckHashes[messageId] ??= [];
|
||||
if (!_expectedAckHashes[messageId]!.any(
|
||||
(hash) => listEquals(hash, ackHash),
|
||||
)) {
|
||||
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
|
||||
if (!_expectedAckHashes[messageId]!.any((hash) => hash == ackHash)) {
|
||||
_expectedAckHashes[messageId]!.add(ackHash);
|
||||
}
|
||||
|
||||
// Calculate timeout: prefer ML prediction, then device-provided, then physics fallback
|
||||
@@ -400,14 +401,11 @@ class MessageRetryService extends ChangeNotifier {
|
||||
|
||||
int actualTimeout = timeoutMs;
|
||||
if (config.calculateTimeout != null) {
|
||||
final calculated = config.calculateTimeout!(
|
||||
actualTimeout = config.calculateTimeout!(
|
||||
pathLengthValue,
|
||||
message.text.length,
|
||||
contactKey: contact.publicKeyHex,
|
||||
);
|
||||
if (timeoutMs <= 0 || calculated < timeoutMs) {
|
||||
actualTimeout = calculated;
|
||||
}
|
||||
}
|
||||
|
||||
final updatedMessage = message.copyWith(
|
||||
@@ -559,10 +557,10 @@ class MessageRetryService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
bool _checkAckHistory(Uint8List ackHash) {
|
||||
bool _checkAckHistory(int ackHash) {
|
||||
for (final entry in _ackHistory) {
|
||||
for (final expectedHash in entry.ackHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
if (expectedHash == ackHash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -570,13 +568,12 @@ class MessageRetryService extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
void handleAckReceived(Uint8List ackHash, int tripTimeMs) {
|
||||
void handleAckReceived(int ackHash, int tripTimeMs) {
|
||||
final config = _config;
|
||||
String? matchedMessageId;
|
||||
int? matchedAttemptIndex;
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
PathSelection? matchedPathSelection;
|
||||
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||
|
||||
// Clean up old ACK hash mappings (older than 15 minutes)
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15));
|
||||
@@ -595,6 +592,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
if (mapping != null) {
|
||||
matchedMessageId = mapping.messageId;
|
||||
matchedAttemptIndex = mapping.attemptIndex;
|
||||
matchedPathSelection = mapping.pathSelection;
|
||||
} else {
|
||||
config?.debugLogService?.warn(
|
||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex not found in direct mapping, trying fallback',
|
||||
@@ -606,7 +604,7 @@ class MessageRetryService extends ChangeNotifier {
|
||||
final expectedHashes = entry.value;
|
||||
|
||||
for (final expectedHash in expectedHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
if (expectedHash == ackHash) {
|
||||
matchedMessageId = messageId;
|
||||
matchedAttemptIndex = expectedHashes.indexOf(expectedHash);
|
||||
break;
|
||||
@@ -625,13 +623,13 @@ class MessageRetryService extends ChangeNotifier {
|
||||
}
|
||||
final contact = _pendingContacts[matchedMessageId];
|
||||
final ackedAttempt = matchedAttemptIndex ?? message.retryCount;
|
||||
final selection = _selectionFromMessage(message);
|
||||
final selection = matchedPathSelection ?? _selectionFromMessage(message);
|
||||
|
||||
final shortText = message.text.length > 20
|
||||
? '${message.text.substring(0, 20)}...'
|
||||
: message.text;
|
||||
config?.debugLogService?.info(
|
||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on retry ${ackedAttempt + 1} in ${tripTimeMs}ms',
|
||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on attempt $ackedAttempt in ${tripTimeMs}ms',
|
||||
tag: 'AckHash',
|
||||
);
|
||||
|
||||
@@ -643,6 +641,8 @@ class MessageRetryService extends ChangeNotifier {
|
||||
tripTimeMs: tripTimeMs,
|
||||
);
|
||||
|
||||
final wasAlreadyResolved = _resolvedMessages.contains(matchedMessageId);
|
||||
|
||||
_cleanupMessage(matchedMessageId);
|
||||
|
||||
config?.updateMessage(deliveredMessage);
|
||||
@@ -665,7 +665,9 @@ class MessageRetryService extends ChangeNotifier {
|
||||
tripTimeMs,
|
||||
);
|
||||
}
|
||||
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
|
||||
if (!wasAlreadyResolved) {
|
||||
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
@@ -685,11 +687,11 @@ class MessageRetryService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String? getContactKeyForAckHash(Uint8List ackHash) {
|
||||
String? getContactKeyForAckHash(int ackHash) {
|
||||
for (var entry in _pendingMessages.entries) {
|
||||
final message = entry.value;
|
||||
if (message.expectedAckHash != null &&
|
||||
listEquals(message.expectedAckHash, ackHash)) {
|
||||
message.expectedAckHash == ackHash) {
|
||||
final contact = _pendingContacts[entry.key];
|
||||
return contact?.publicKeyHex;
|
||||
}
|
||||
|
||||
@@ -565,6 +565,16 @@ class PathHistoryService extends ChangeNotifier {
|
||||
_floodStats.remove(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
void clearAllHistories() {
|
||||
_cache.clear();
|
||||
_cacheAccessOrder.clear();
|
||||
_autoRotationIndex.clear();
|
||||
_floodStats.clear();
|
||||
_storage.clearAllPathHistories();
|
||||
_version = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class _DeferredPathRecord {
|
||||
|
||||
@@ -273,7 +273,7 @@ class UsbSerialService {
|
||||
throw StateError('USB serial port is not open');
|
||||
}
|
||||
final packet = wrapUsbSerialTxFrame(data);
|
||||
_logFrameSummary('USB TX frame', data);
|
||||
// _logFrameSummary('USB TX frame', data);
|
||||
if (_useAndroidUsbHost) {
|
||||
try {
|
||||
await _androidMethodChannel.invokeMethod<void>('write', {
|
||||
@@ -447,16 +447,16 @@ class UsbSerialService {
|
||||
await _frameController.close();
|
||||
}
|
||||
|
||||
void _logFrameSummary(String prefix, Uint8List bytes) {
|
||||
if (bytes.isEmpty) {
|
||||
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
|
||||
return;
|
||||
}
|
||||
_debugLogService?.info(
|
||||
'$prefix code=${bytes[0]} len=${bytes.length}',
|
||||
tag: 'USB Serial',
|
||||
);
|
||||
}
|
||||
// void _logFrameSummary(String prefix, Uint8List bytes) {
|
||||
// if (bytes.isEmpty) {
|
||||
// _debugLogService?.info('$prefix len=0', tag: 'USB Serial');
|
||||
// return;
|
||||
// }
|
||||
// _debugLogService?.info(
|
||||
// '$prefix code=${bytes[0]} len=${bytes.length}',
|
||||
// tag: 'USB Serial',
|
||||
// );
|
||||
// }
|
||||
|
||||
/// Returns an ordered list of port paths to try for [portName].
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user