From e7a481f97bf288c6c0a25987440bd85c32b8fadb Mon Sep 17 00:00:00 2001 From: Janez T Date: Fri, 13 Mar 2026 20:32:07 +0100 Subject: [PATCH] Warn channels for delayed log rx --- ios/Runner.xcodeproj/project.pbxproj | 12 +-- ios/Runner/Info.plist | 2 +- ios/fastlane/report.xml | 10 ++- lib/utils/trace_node_resolver.dart | 89 +++++++++++++++++++ lib/widgets/contacts/contact_trace_sheet.dart | 7 +- lib/widgets/messages/message_trace_sheet.dart | 7 +- pubspec.yaml | 2 +- test/utils/trace_node_resolver_test.dart | 59 ++++++++++++ 8 files changed, 174 insertions(+), 14 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index acc0526..6ffe0a4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -530,7 +530,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -547,7 +547,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = JND55328G8; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -679,7 +679,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -702,7 +702,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3d869bf..96d2207 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -43,7 +43,7 @@ CFBundleSignature ???? CFBundleVersion - 113 + 115 LSRequiresIPhoneOS ITSAppUsesNonExemptEncryption diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index 5563930..13832ca 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,24 @@ - + - + - + - + + + diff --git a/lib/utils/trace_node_resolver.dart b/lib/utils/trace_node_resolver.dart index a29808c..be9d22a 100644 --- a/lib/utils/trace_node_resolver.dart +++ b/lib/utils/trace_node_resolver.dart @@ -108,6 +108,87 @@ class TraceNodeResolver { ); } + static List alignPathSelections({ + required List nodes, + MeshMapNode? startNode, + MeshMapNode? endNode, + }) { + if (nodes.isEmpty || nodes.any((node) => node.candidates.isEmpty)) { + return nodes; + } + + final candidateCosts = List.generate( + nodes.length, + (_) => [], + growable: false, + ); + final previousChoice = List.generate( + nodes.length, + (_) => [], + growable: false, + ); + + for (var i = 0; i < nodes.length; i++) { + final currentCandidates = nodes[i].candidates; + candidateCosts[i] = List.filled( + currentCandidates.length, + double.infinity, + ); + previousChoice[i] = List.filled(currentCandidates.length, -1); + + for (var j = 0; j < currentCandidates.length; j++) { + final current = currentCandidates[j]; + if (i == 0) { + candidateCosts[i][j] = startNode == null + ? 0 + : _distanceBetweenNodes(startNode, current); + continue; + } + + final previousCandidates = nodes[i - 1].candidates; + for (var k = 0; k < previousCandidates.length; k++) { + final candidateCost = + candidateCosts[i - 1][k] + + _distanceBetweenNodes(previousCandidates[k], current); + if (candidateCost < candidateCosts[i][j]) { + candidateCosts[i][j] = candidateCost; + previousChoice[i][j] = k; + } + } + } + } + + var bestLastIndex = 0; + var bestLastCost = double.infinity; + final lastCandidates = nodes.last.candidates; + for (var i = 0; i < lastCandidates.length; i++) { + final endCost = endNode == null + ? 0 + : _distanceBetweenNodes(lastCandidates[i], endNode); + final totalCost = candidateCosts.last[i] + endCost; + if (totalCost < bestLastCost) { + bestLastCost = totalCost; + bestLastIndex = i; + } + } + + final selectedIndices = List.filled(nodes.length, 0); + selectedIndices[nodes.length - 1] = bestLastIndex; + for (var i = nodes.length - 1; i > 0; i--) { + selectedIndices[i - 1] = previousChoice[i][selectedIndices[i]]; + } + + return List.generate(nodes.length, (index) { + final resolved = nodes[index]; + return ResolvedTraceNode( + candidates: resolved.candidates, + matchCount: resolved.matchCount, + usedOnlineFallback: resolved.usedOnlineFallback, + selectedIndex: selectedIndices[index], + ); + }, growable: false); + } + static double _scoreNode( MeshMapNode node, { LatLng? referenceA, @@ -126,6 +207,14 @@ class TraceNodeResolver { return double.maxFinite; } + static double _distanceBetweenNodes(MeshMapNode a, MeshMapNode b) { + return _distance.as( + LengthUnit.Meter, + LatLng(a.latitude, a.longitude), + LatLng(b.latitude, b.longitude), + ); + } + static double _distanceToSegmentMeters(LatLng p, LatLng a, LatLng b) { final ax = a.longitude; final ay = a.latitude; diff --git a/lib/widgets/contacts/contact_trace_sheet.dart b/lib/widgets/contacts/contact_trace_sheet.dart index 9205f87..9d9883c 100644 --- a/lib/widgets/contacts/contact_trace_sheet.dart +++ b/lib/widgets/contacts/contact_trace_sheet.dart @@ -420,12 +420,17 @@ class _ContactTraceSheetState extends State { ), ) .toList(); + final alignedRelayNodes = TraceNodeResolver.alignPathSelections( + nodes: matchedRelayNodes, + startNode: senderNode.node, + endNode: recipientNode.node, + ); return _ContactTraceResult( sender: senderNode, recipient: recipientNode, routeHashes: routeHashes, - matchedRelayNodes: matchedRelayNodes, + matchedRelayNodes: alignedRelayNodes, ); } diff --git a/lib/widgets/messages/message_trace_sheet.dart b/lib/widgets/messages/message_trace_sheet.dart index f528cb3..8b90577 100644 --- a/lib/widgets/messages/message_trace_sheet.dart +++ b/lib/widgets/messages/message_trace_sheet.dart @@ -531,12 +531,17 @@ class _MessageTraceSheetState extends State { senderLatLng: senderLatLng, recipientLatLng: recipientLatLng, ); + final alignedMatched = TraceNodeResolver.alignPathSelections( + nodes: matched, + startNode: senderNode.node, + endNode: recipientNode.node, + ); return _TraceResult( mode: TraceMode.packetPath, sender: senderNode, recipient: recipientNode, pathHashes: hopHashes, - matchedPathNodes: matched, + matchedPathNodes: alignedMatched, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 2cadca7..9f50894 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2026.0313.1+28 +version: 2026.0313.2+29 environment: sdk: ^3.9.2 diff --git a/test/utils/trace_node_resolver_test.dart b/test/utils/trace_node_resolver_test.dart index be322f2..ec58feb 100644 --- a/test/utils/trace_node_resolver_test.dart +++ b/test/utils/trace_node_resolver_test.dart @@ -88,6 +88,65 @@ void main() { expect(resolved.cycle().node?.name, 'First Match'); expect(resolved.cycle().cycle().node?.name, 'Second Match'); }); + + test('aligns ambiguous hops to the closest continuous path', () { + final start = _node( + name: 'Start', + publicKey: 'start00', + latitude: 46.000, + longitude: 14.000, + ); + final end = _node( + name: 'End', + publicKey: 'end000', + latitude: 46.300, + longitude: 14.300, + ); + final hop1Near = _node( + name: 'Hop 1 Near', + publicKey: 'aa1100', + latitude: 46.100, + longitude: 14.100, + ); + final hop1Far = _node( + name: 'Hop 1 Far', + publicKey: 'aa11ff', + latitude: 46.250, + longitude: 14.000, + ); + final hop2Near = _node( + name: 'Hop 2 Near', + publicKey: 'bb2200', + latitude: 46.200, + longitude: 14.200, + ); + final hop2Far = _node( + name: 'Hop 2 Far', + publicKey: 'bb22ff', + latitude: 46.050, + longitude: 14.280, + ); + + final aligned = TraceNodeResolver.alignPathSelections( + nodes: [ + TraceNodeResolver.resolveBest( + nodes: [hop1Far, hop1Near], + localPublicKeys: {hop1Near.publicKey, hop1Far.publicKey}, + prefixHex: 'aa11', + ), + TraceNodeResolver.resolveBest( + nodes: [hop2Far, hop2Near], + localPublicKeys: {hop2Near.publicKey, hop2Far.publicKey}, + prefixHex: 'bb22', + ), + ], + startNode: start, + endNode: end, + ); + + expect(aligned[0].node?.name, 'Hop 1 Near'); + expect(aligned[1].node?.name, 'Hop 2 Near'); + }); } MeshMapNode _node({