@ -7,6 +7,7 @@ import 'package:flutter_map/flutter_map.dart';
import ' package:latlong2/latlong.dart ' ;
import ' package:latlong2/latlong.dart ' ;
import ' package:meshcore_open/connector/meshcore_connector.dart ' ;
import ' package:meshcore_open/connector/meshcore_connector.dart ' ;
import ' package:meshcore_open/connector/meshcore_protocol.dart ' ;
import ' package:meshcore_open/connector/meshcore_protocol.dart ' ;
import ' package:meshcore_open/helpers/path_helper.dart ' ;
import ' package:meshcore_open/l10n/l10n.dart ' ;
import ' package:meshcore_open/l10n/l10n.dart ' ;
import ' package:meshcore_open/models/app_settings.dart ' ;
import ' package:meshcore_open/models/app_settings.dart ' ;
import ' package:meshcore_open/models/contact.dart ' ;
import ' package:meshcore_open/models/contact.dart ' ;
@ -41,11 +42,18 @@ class PathTraceData {
final List < double > snrData ;
final List < double > snrData ;
final Map < int , Contact > pathContacts ;
final Map < int , Contact > pathContacts ;
/ / / Bytes per hop ( 1 < < path_sz ) parsed from the trace response flag . ( # 150 )
final int hopBytes ;
PathTraceData ( {
PathTraceData ( {
required this . pathData ,
required this . pathData ,
required this . snrData ,
required this . snrData ,
required this . pathContacts ,
required this . pathContacts ,
this . hopBytes = 1 ,
} ) ;
} ) ;
/ / / Path hops as packed big - endian prefixes of [ hopBytes ] each . ( # 150 )
List < int > get hops = > PathHelper . tracePathHops ( pathData , hopBytes ) ;
}
}
class PathTraceMapScreen extends StatefulWidget {
class PathTraceMapScreen extends StatefulWidget {
@ -188,44 +196,40 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}
}
Uint8List buildPath ( Uint8List pathBytes ) {
Uint8List buildPath ( Uint8List pathBytes ) {
Uint8List traceBytes ;
final hopBytes = PathHelper . traceHopBytes ( widget . pathHashByteWidth ) ;
final pk = widget . targetContact ? . publicKey ;
if ( pathBytes . isEmpty ) {
if ( pathBytes . isEmpty ) {
final pk = widget . targetContact ? . publicKey ;
/ / Direct target: send its own width prefix .
final n = widget . pathHashByteWidth . clamp ( 1 , pubKeySize ) ;
if ( pk ! = null & & pk . length > = hopBytes ) {
if ( pk ! = null & & pk . length > = n ) {
return Uint8List . fromList ( pk . sublist ( 0 , hopBytes ) ) ;
return Uint8List . fromList ( pk . sublist ( 0 , n ) ) ;
}
}
traceBytes = Uint8List ( 1 ) ;
return Uint8List . fromList ( [ pk ! = null & & pk . isNotEmpty ? pk [ 0 ] : 0 ] ) ;
traceBytes [ 0 ] = pk ? [ 0 ] ? ? 0 ;
return traceBytes ;
}
}
/ / An all - zero path is the " no known path " marker → no trace payload .
if ( widget . targetContact ? . type = = advTypeRepeater | |
if ( pathBytes . every ( ( b ) = > b = = 0 ) ) {
widget . targetContact ? . type = = advTypeRoom ) {
return Uint8List ( 0 ) ;
final len = ( pathBytes . length + pathBytes . length + 1 ) ;
traceBytes = Uint8List ( len ) ;
traceBytes [ pathBytes . length ] = widget . targetContact ? . publicKey [ 0 ] ? ? 0 ;
for ( int i = 0 ; i < pathBytes . length ; i + + ) {
traceBytes [ i ] = pathBytes [ i ] ;
if ( i < pathBytes . length ) {
traceBytes [ len - 1 - i ] = pathBytes [ i ] ;
}
}
} else {
if ( pathBytes . length < 2 ) {
return pathBytes [ 0 ] = = 0 ? Uint8List ( 0 ) : pathBytes ;
}
final len = ( pathBytes . length + pathBytes . length - 1 ) ;
traceBytes = Uint8List ( len ) ;
for ( int i = 0 ; i < pathBytes . length ; i + + ) {
traceBytes [ i ] = pathBytes [ i ] ;
if ( i < pathBytes . length - 1 ) {
traceBytes [ len - 1 - i ] = pathBytes [ i ] ;
}
}
}
}
return traceBytes ;
final throughTarget =
widget . targetContact ? . type = = advTypeRepeater | |
widget . targetContact ? . type = = advTypeRoom ;
final targetPrefix = throughTarget
? ( pk ! = null & & pk . length > = hopBytes
? pk . sublist ( 0 , hopBytes )
: < int > [ pk ! = null & & pk . isNotEmpty ? pk [ 0 ] : 0 ] )
: const < int > [ ] ;
return Uint8List . fromList (
PathHelper . buildTraceRoundTrip (
routingPath: pathBytes ,
routingWidth: widget . pathHashByteWidth < 1
? 1
: widget . pathHashByteWidth ,
hopBytes: hopBytes ,
throughTarget: throughTarget ,
targetPrefix: targetPrefix ,
) ,
) ;
}
}
Future < void > _doPathTrace ( ) async {
Future < void > _doPathTrace ( ) async {
@ -249,10 +253,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
) ;
) ;
final connector = Provider . of < MeshCoreConnector > ( context , listen: false ) ;
final connector = Provider . of < MeshCoreConnector > ( context , listen: false ) ;
/ / path_sz tells the firmware the hop width ; the payload hops are built at
/ / the matching width ( 1 < < path_sz bytes each ) . ( # 150 )
final pathSz = PathHelper . tracePathSz ( widget . pathHashByteWidth ) ;
final frame = buildTraceReq (
final frame = buildTraceReq (
DateTime . now ( ) . millisecondsSinceEpoch ~ / 1000 ,
DateTime . now ( ) . millisecondsSinceEpoch ~ / 1000 ,
0 , / / flags
0 , / / auth
0 , / / auth
pathSz , / / flag
payload: path ,
payload: path ,
) ;
) ;
connector . sendFrame ( frame ) ;
connector . sendFrame ( frame ) ;
@ -327,9 +334,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
try {
try {
buffer . skipBytes ( 2 ) ; / / Skip push code and reserved byte
buffer . skipBytes ( 2 ) ; / / Skip push code and reserved byte
int pathLength = buffer . readUInt8 ( ) ;
int pathLength = buffer . readUInt8 ( ) ;
buffer . skipBytes ( 5 ) ; / / Skip Flag byte and tag data
final pathSz = buffer . readUInt8 ( ) & 0x03 ; / / flag byte → trace hop width
buffer . skipBytes ( 4 ) ; / / Skip auth code
final hopBytes = 1 < < pathSz ;
buffer . skipBytes ( 4 ) ; / / tag data
buffer . skipBytes ( 4 ) ; / / auth code
Uint8List pathData = buffer . readBytes ( pathLength ) ;
Uint8List pathData = buffer . readBytes ( pathLength ) ;
final hopList = PathHelper . tracePathHops ( pathData , hopBytes ) ;
/ / One SNR per hop , plus a trailing final SNR .
List < double > snrData = buffer
List < double > snrData = buffer
. readRemainingBytes ( )
. readRemainingBytes ( )
. map ( ( snr ) = > snr . toSigned ( 8 ) . toDouble ( ) / 4 )
. map ( ( snr ) = > snr . toSigned ( 8 ) . toDouble ( ) / 4 )
@ -347,7 +358,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
lastSeen: DateTime . now ( ) ,
lastSeen: DateTime . now ( ) ,
) ;
) ;
if ( widget . pathContacts ! = null ) {
if ( widget . pathContacts ! = null ) {
pathContacts = { for ( var c in widget . pathContacts ! ) c . publicKey [ 0 ] : c } ;
pathContacts = {
for ( var c in widget . pathContacts ! )
if ( c . publicKey . length > = hopBytes )
PathHelper . packPrefix ( c . publicKey , 0 , hopBytes ) : c ,
} ;
} else {
} else {
final contacts = connector . allContactsUnfiltered ;
final contacts = connector . allContactsUnfiltered ;
contacts . where ( ( c ) = > c . type ! = advTypeChat ) . forEach ( ( repeater ) {
contacts . where ( ( c ) = > c . type ! = advTypeChat ) . forEach ( ( repeater ) {
@ -362,14 +377,15 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
_maxRepeaterMatchDistanceMeters ) {
_maxRepeaterMatchDistanceMeters ) {
return ; / / skip reapeaters that are far away from the last one with known GPS , to avoid false matches
return ; / / skip reapeaters that are far away from the last one with known GPS , to avoid false matches
}
}
for ( var repeaterData in pathData ) {
if ( repeater . publicKey . length < hopBytes ) return ;
if ( listEquals (
final repeaterHop = PathHelper . packPrefix (
repeater . publicKey . sublist ( 0 , 1 ) ,
repeater . publicKey ,
Uint8List . fromList ( [ repeaterData ] ) ,
0 ,
) ) {
hopBytes ,
pathContacts [ repeaterData ] = repeater ;
) ;
lastContact = repeater ;
if ( hopList . contains ( repeaterHop ) ) {
}
pathContacts [ repeaterHop ] = repeater ;
lastContact = repeater ;
}
}
} ) ;
} ) ;
}
}
@ -377,12 +393,20 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
/ / For hops with no GPS contact , infer position from other contacts
/ / For hops with no GPS contact , infer position from other contacts
/ / with known GPS that share the same last - hop byte .
/ / with known GPS that share the same last - hop byte .
final Map < int , LatLng > inferredPositions = { } ;
final Map < int , LatLng > inferredPositions = { } ;
for ( final hop in pathData ) {
for ( final hop in hopList ) {
final contact = pathContacts [ hop ] ;
final contact = pathContacts [ hop ] ;
if ( contact ! = null & & contact . hasLocation ) continue ;
if ( contact ! = null & & contact . hasLocation ) continue ;
final peers = connector . contacts
final peers = connector . contacts
. where (
. where (
( c ) = > c . hasLocation & & c . path . isNotEmpty & & c . path . last = = hop ,
( c ) = >
c . hasLocation & &
c . path . length > = hopBytes & &
PathHelper . packPrefix (
c . path ,
c . path . length - hopBytes ,
hopBytes ,
) = =
hop ,
)
)
. toList ( ) ;
. toList ( ) ;
if ( peers . isNotEmpty ) {
if ( peers . isNotEmpty ) {
@ -404,6 +428,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
pathData: pathData ,
pathData: pathData ,
snrData: snrData ,
snrData: snrData ,
pathContacts: pathContacts ,
pathContacts: pathContacts ,
hopBytes: hopBytes ,
) ;
) ;
/ / Compute endpoint position for the target contact .
/ / Compute endpoint position for the target contact .
LatLng ? targetPos ;
LatLng ? targetPos ;
@ -418,16 +443,24 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
/ / Infer from the last hop: average GPS contacts sharing that hop .
/ / Infer from the last hop: average GPS contacts sharing that hop .
/ / For a round - trip path ( flipPathAround / reversePathAround ) , the target - side hop
/ / For a round - trip path ( flipPathAround / reversePathAround ) , the target - side hop
/ / sits in the middle of the symmetric sequence ; . last is the local side .
/ / sits in the middle of the symmetric sequence ; . last is the local side .
final lastHop = widget . reversePathAround
final hb = PathHelper . traceHopBytes ( widget . pathHashByteWidth ) ;
? widget . path . first
final lastHop = widget . path . length < hb
: widget . path . last ;
? 0
: ( widget . reversePathAround
? PathHelper . packPrefix ( widget . path , 0 , hb )
: PathHelper . packPrefix (
widget . path ,
widget . path . length - hb ,
hb ,
) ) ;
final peers = connector . allContacts
final peers = connector . allContacts
. where (
. where (
( c ) = >
( c ) = >
c . hasLocation & &
c . hasLocation & &
c . path . isNotEmpty & &
c . path . length > = hb & &
c . path . last = = lastHop ,
PathHelper . packPrefix ( c . path , c . path . length - hb , hb ) = =
lastHop ,
)
)
. toList ( ) ;
. toList ( ) ;
if ( peers . isNotEmpty ) {
if ( peers . isNotEmpty ) {
@ -476,7 +509,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
_points . add ( LatLng ( connector . selfLatitude ! , connector . selfLongitude ! ) ) ;
_points . add ( LatLng ( connector . selfLatitude ! , connector . selfLongitude ! ) ) ;
int hopLast = 0 ;
int hopLast = 0 ;
int hopLastLast = 0 ;
int hopLastLast = 0 ;
for ( final hop in _traceData ! . pathData ) {
for ( final hop in _traceData ! . hops ) {
if ( hop = = hopLastLast & & widget . flipPathAround ) {
if ( hop = = hopLastLast & & widget . flipPathAround ) {
break ; / / skip duplicate hops in round - trip paths
break ; / / skip duplicate hops in round - trip paths
}
}
@ -613,14 +646,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}
}
List < Marker > _buildHopMarkers (
List < Marker > _buildHopMarkers (
List < int > pathData , {
List < int > hops , {
required bool showLabels ,
required bool showLabels ,
required Contact ? target ,
required Contact ? target ,
} ) {
} ) {
final markers = < Marker > [ ] ;
final markers = < Marker > [ ] ;
int hopLast = 0 ;
int hopLast = 0 ;
int hopLastLast = 0 ;
int hopLastLast = 0 ;
for ( final hop in pathData ) {
for ( final hop in hops ) {
final contact = _traceData ! . pathContacts [ hop ] ;
final contact = _traceData ! . pathContacts [ hop ] ;
final inferred = _inferredHopPositions [ hop ] ;
final inferred = _inferredHopPositions [ hop ] ;
final hasGps = contact ! = null & & contact . hasLocation ;
final hasGps = contact ! = null & & contact . hasLocation ;
@ -635,7 +668,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final point = hasGps
final point = hasGps
? LatLng ( contact . latitude ! , contact . longitude ! )
? LatLng ( contact . latitude ! , contact . longitude ! )
: inferred ! ;
: inferred ! ;
final label = hop . toRadixString ( 16 ) . padLeft ( 2 , ' 0 ' ) . toUpperCase ( ) ;
final label = hop
. toRadixString ( 16 )
. padLeft ( _traceData ! . hopBytes * 2 , ' 0 ' )
. toUpperCase ( ) ;
markers . add (
markers . add (
Marker (
Marker (
@ -807,29 +843,23 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}
}
String formatDirectionText ( PathTraceData pathTraceData , int index ) {
String formatDirectionText ( PathTraceData pathTraceData , int index ) {
final hops = pathTraceData . hops ;
final hexLen = pathTraceData . hopBytes * 2 ;
if ( index = = 0 | | index = = pathTraceData . snrData . length - 1 ) {
if ( index = = 0 | | index = = pathTraceData . snrData . length - 1 ) {
if ( index = = 0 ) {
if ( index = = 0 ) {
return context . l10n . pathTrace_you ;
return context . l10n . pathTrace_you ;
} else {
} else {
final contactName = pathTraceData
final hop = hops . isNotEmpty ? hops . last : 0 ;
. pathContacts [ pathTraceData . pathData [ pathTraceData . pathData . length -
final contactName = pathTraceData . pathContacts [ hop ] ? . name ;
1 ] ]
final hex = hop . toRadixString ( 16 ) . padLeft ( hexLen , ' 0 ' ) . toUpperCase ( ) ;
? . name ;
final hex = pathTraceData . pathData [ pathTraceData . pathData . length - 1 ]
. toRadixString ( 16 )
. padLeft ( 2 , ' 0 ' )
. toUpperCase ( ) ;
return contactName ! = null
return contactName ! = null
? " $ hex : $ contactName "
? " $ hex : $ contactName "
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
}
}
} else {
} else {
final contactName =
final hop = index - 1 < hops . length ? hops [ index - 1 ] : 0 ;
pathTraceData . pathContacts [ pathTraceData . pathData [ index - 1 ] ] ? . name ;
final contactName = pathTraceData . pathContacts [ hop ] ? . name ;
final hex = pathTraceData . pathData [ index - 1 ]
final hex = hop . toRadixString ( 16 ) . padLeft ( hexLen , ' 0 ' ) . toUpperCase ( ) ;
. toRadixString ( 16 )
. padLeft ( 2 , ' 0 ' )
. toUpperCase ( ) ;
return contactName ! = null
return contactName ! = null
? " $ hex : $ contactName "
? " $ hex : $ contactName "
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
@ -837,14 +867,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}
}
String formatDirectionSubText ( PathTraceData pathTraceData , int index ) {
String formatDirectionSubText ( PathTraceData pathTraceData , int index ) {
final hops = pathTraceData . hops ;
final hexLen = pathTraceData . hopBytes * 2 ;
if ( index = = 0 | | index = = pathTraceData . snrData . length - 1 ) {
if ( index = = 0 | | index = = pathTraceData . snrData . length - 1 ) {
if ( index = = 0 ) {
if ( index = = 0 ) {
final contactName =
final hop = hops . isNotEmpty ? hops . first : 0 ;
pathTraceData . pathContacts [ pathTraceData . pathData [ 0 ] ] ? . name ;
final contactName = pathTraceData . pathContacts [ hop ] ? . name ;
final hex = pathTraceData . pathData [ 0 ]
final hex = hop . toRadixString ( 16 ) . padLeft ( hexLen , ' 0 ' ) . toUpperCase ( ) ;
. toRadixString ( 16 )
. padLeft ( 2 , ' 0 ' )
. toUpperCase ( ) ;
return contactName ! = null
return contactName ! = null
? " $ hex : $ contactName "
? " $ hex : $ contactName "
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
@ -852,12 +881,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
return context . l10n . pathTrace_you ;
return context . l10n . pathTrace_you ;
}
}
} else {
} else {
final contactName =
final hop = index < hops . length ? hops [ index ] : 0 ;
pathTraceData . pathContacts [ pathTraceData . pathData [ index ] ] ? . name ;
final contactName = pathTraceData . pathContacts [ hop ] ? . name ;
final hex = pathTraceData . pathData [ index ]
final hex = hop . toRadixString ( 16 ) . padLeft ( hexLen , ' 0 ' ) . toUpperCase ( ) ;
. toRadixString ( 16 )
. padLeft ( 2 , ' 0 ' )
. toUpperCase ( ) ;
return contactName ! = null
return contactName ! = null
? " $ hex : $ contactName "
? " $ hex : $ contactName "
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
: " $ hex : ${ context . l10n . channelPath_unknownRepeater } " ;
@ -915,10 +941,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
maxZoom: 19 ,
maxZoom: 19 ,
) ,
) ,
if ( _polylines . isNotEmpty ) PolylineLayer ( polylines: _polylines ) ,
if ( _polylines . isNotEmpty ) PolylineLayer ( polylines: _polylines ) ,
if ( _traceData ! . pathData . isNotEmpty )
if ( _traceData ! . hops . isNotEmpty )
MarkerLayer (
MarkerLayer (
markers: _buildHopMarkers (
markers: _buildHopMarkers (
_traceData ! . pathData ,
_traceData ! . hops ,
showLabels: _showNodeLabels ,
showLabels: _showNodeLabels ,
target: target ,
target: target ,
) ,
) ,
@ -934,7 +960,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
) {
) {
final l10n = context . l10n ;
final l10n = context . l10n ;
final maxHeight = MediaQuery . of ( context ) . size . height * 0.35 ;
final maxHeight = MediaQuery . of ( context ) . size . height * 0.35 ;
final estimatedHeight = 72.0 + ( pathTraceData . pathData . length * 56.0 ) ;
final estimatedHeight = 72.0 + ( pathTraceData . hops . length * 56.0 ) ;
final cardHeight = max ( 96.0 , min ( maxHeight , estimatedHeight ) ) ;
final cardHeight = max ( 96.0 , min ( maxHeight , estimatedHeight ) ) ;
return Positioned (
return Positioned (
@ -956,14 +982,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
) ,
) ,
const Divider ( height: 1 ) ,
const Divider ( height: 1 ) ,
Expanded (
Expanded (
child: pathTraceData . pathData . isEmpty
child: pathTraceData . hops . isEmpty
? Center (
? Center (
child: Text ( l10n . channelPath_noHopDetailsAvailable ) ,
child: Text ( l10n . channelPath_noHopDetailsAvailable ) ,
)
)
: Scrollbar (
: Scrollbar (
child: ListView . separated (
child: ListView . separated (
padding: const EdgeInsets . symmetric ( vertical: 4 ) ,
padding: const EdgeInsets . symmetric ( vertical: 4 ) ,
itemCount: pathTraceData . pathData . length + 1 ,
itemCount: pathTraceData . hops . length + 1 ,
separatorBuilder: ( _ , _ ) = > const Divider ( height: 1 ) ,
separatorBuilder: ( _ , _ ) = > const Divider ( height: 1 ) ,
itemBuilder: ( context , index ) {
itemBuilder: ( context , index ) {
final snrUi = snrUiFromSNR (
final snrUi = snrUiFromSNR (