Moved all the path tracing logic to the dialog.

refactored repeater hub along with contacts screen to use shortPubKeyHex.
Added localization strings for path tracing, english only.
chore/offband-rebrand
Winston Lowe 6 months ago
parent 0ebd688787
commit cacb9bc677

@ -1308,5 +1308,24 @@
"listFilter_repeaters": "Repeaters", "listFilter_repeaters": "Repeaters",
"listFilter_roomServers": "Room servers", "listFilter_roomServers": "Room servers",
"listFilter_unreadOnly": "Unread only", "listFilter_unreadOnly": "Unread only",
"listFilter_newGroup": "New group" "listFilter_newGroup": "New group",
"pathTrace_you": "You",
"pathTrace_failed": "Path trace failed.",
"pathTrace_notAvailable": "Path trace not available.",
"pathTrace_refreshTooltip": "Refresh Path Trace.",
"contacts_pathTrace": "Path Trace",
"contacts_ping": "Ping",
"contacts_repeaterPathTrace": "Path trace to repeater",
"contacts_repeaterPing": "Ping repeater",
"contacts_roomPathTrace": "Path trace to room server",
"contacts_roomPing": "Ping room server",
"contacts_chatTraceRoute": "Path trace route",
"contacts_pathTraceTo": "Trace route to {name}",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {"type": "String"}
}
}
} }

@ -54,14 +54,11 @@ class _ContactsScreenState extends State<ContactsScreen>
final ContactGroupStore _groupStore = ContactGroupStore(); final ContactGroupStore _groupStore = ContactGroupStore();
List<ContactGroup> _groups = []; List<ContactGroup> _groups = [];
Timer? _searchDebounce; Timer? _searchDebounce;
StreamSubscription<Uint8List>? _frameSubscription;
Uint8List _tagData = Uint8List(4);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadGroups(); _loadGroups();
_setupFrameListener();
} }
@override @override
@ -71,44 +68,6 @@ class _ContactsScreenState extends State<ContactsScreen>
super.dispose(); super.dispose();
} }
void _setupFrameListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
print("Stored tag data: $_tagData");
}
// Check if it's a binary response
if (frame[0] == pushCodeTraceData && listEquals(frame.sublist(4, 8), _tagData)) {
if (!mounted) return;
_handleTraceResponse(frame);
}
});
}
Future<void> _handleTraceResponse(Uint8List frame)async {
final buffer = BufferReader(frame);
buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8();
buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code
Uint8List pathData = buffer.readBytes(pathLength);
Uint8List snrData = buffer.readRemainingBytes();
print("Received path data length: $pathLength, SNR data length: ${snrData.length}");
showDialog(
context: context,
builder: (context) => PathTraceDialog(
pathData: pathData,
snrData: snrData,
),
);
}
Future<void> _loadGroups() async { Future<void> _loadGroups() async {
final groups = await _groupStore.loadGroups(); final groups = await _groupStore.loadGroups();
if (!mounted) return; if (!mounted) return;
@ -799,16 +758,14 @@ class _ContactsScreenState extends State<ContactsScreen>
if (isRepeater) ...[ if (isRepeater) ...[
ListTile( ListTile(
leading: const Icon(Icons.radar, color: Colors.green), leading: const Icon(Icons.radar, color: Colors.green),
title: Text("Ping"), title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
onTap: () async { onTap: () {
Navigator.pop(sheetContext); showDialog(context: context, builder: (context) {
final frame = buildTraceReq( return PathTraceDialog(
DateTime.now().millisecondsSinceEpoch ~/ 1000, title: contact.pathLength > 0 ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing,
0, path: contact.traceRouteBytes ?? Uint8List(0),
0, );
payload: Uint8List.fromList([0x85,0x91,0x07,0x91,0x85]) //contact.publicKey.sublist(0,1), });
);
await connector.sendFrame(frame);
} }
), ),
ListTile( ListTile(
@ -822,15 +779,14 @@ class _ContactsScreenState extends State<ContactsScreen>
]else if (isRoom) ...[ ]else if (isRoom) ...[
ListTile( ListTile(
leading: const Icon(Icons.radar, color: Colors.green), leading: const Icon(Icons.radar, color: Colors.green),
title: Text("Ping"), title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
onTap: () async { onTap: () {
final frame = buildTraceReq( showDialog(context: context, builder: (context) {
DateTime.now().millisecondsSinceEpoch ~/ 1000, return PathTraceDialog(
0, title: contact.pathLength > 0 ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing,
0, path: contact.traceRouteBytes ?? Uint8List(0),
payload: contact.publicKey.sublist(0,1), );
); });
await connector.sendFrame(frame);
} }
), ),
ListTile( ListTile(
@ -849,7 +805,20 @@ class _ContactsScreenState extends State<ContactsScreen>
_showRoomLogin(context, contact, RoomLoginDestination.management); _showRoomLogin(context, contact, RoomLoginDestination.management);
}, },
), ),
] else ] else ...[
if(contact.pathLength > 0)
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: Text(context.l10n.contacts_chatTraceRoute),
onTap: () {
showDialog(context: context, builder: (context) {
return PathTraceDialog(
title: context.l10n.contacts_pathTraceTo(contact.name),
path: contact.traceRouteBytes ?? Uint8List(0),
);
});
}
),
ListTile( ListTile(
leading: const Icon(Icons.chat), leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat), title: Text(context.l10n.contacts_openChat),
@ -869,6 +838,7 @@ class _ContactsScreenState extends State<ContactsScreen>
_confirmDelete(context, connector, contact); _confirmDelete(context, connector, contact);
}, },
), ),
],
], ],
), ),
), ),
@ -923,8 +893,6 @@ class _ContactTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final shotPublicKey =
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
return ListTile( return ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type), backgroundColor: _getTypeColor(contact.type),
@ -932,7 +900,7 @@ class _ContactTile extends StatelessWidget {
), ),
title: Text(contact.name), title: Text(contact.name),
subtitle: Text( subtitle: Text(
'${contact.typeLabel}${contact.pathLabel} $shotPublicKey', '${contact.typeLabel}${contact.pathLabel} ${contact.shortPubKeyHex}',
), ),
trailing: Column( trailing: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

@ -73,7 +73,7 @@ class RepeaterHubScreen extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>', '$repeater.shortPubKeyHex',
style: TextStyle(fontSize: 14, color: Colors.grey[600]), style: TextStyle(fontSize: 14, color: Colors.grey[600]),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

@ -1,50 +1,221 @@
import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/widgets/snr_indicator.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../models/contact.dart';
import '../widgets/snr_indicator.dart';
import '../l10n/l10n.dart';
class PathTraceDialog extends StatefulWidget { class PathTraceDialog extends StatefulWidget {
const PathTraceDialog({ const PathTraceDialog({
super.key, super.key,
required this.pathData, required this.title,
required this.snrData, required this.path,
}); });
final Uint8List pathData; final String title;
final Uint8List snrData; final Uint8List path;
@override @override
State<PathTraceDialog> createState() => _PathTraceDialogState(); State<PathTraceDialog> createState() => _PathTraceDialogState();
} }
class _PathTraceDialogState extends State<PathTraceDialog> { class _PathTraceDialogState extends State<PathTraceDialog> {
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
bool _isLoading = false;
bool _failed2Loaded = false;
bool _hasData = false;
Uint8List _pathData = Uint8List(0);
Uint8List _snrData = Uint8List(0) ;
Map<int, Contact> _pathContacts = {};
@override
void initState() {
super.initState();
_setupFrameListener();
_doPathTrace();
}
@override
void dispose() {
_frameSubscription?.cancel();
_timeoutTimer?.cancel();
super.dispose();
}
Future<void> _doPathTrace() async {
if(mounted) {
setState(() {
_isLoading = true;
_failed2Loaded = false;
});
}
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
0, //flags
0, //auth
payload: widget.path,
);
connector.sendFrame(frame);
}
void _setupFrameListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
Uint8List tagData = Uint8List(4);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final frameBuffer = BufferReader(frame);
final code = frameBuffer.readUInt8();
if (code == respCodeSent) {
frameBuffer.skipBytes(1); //reserved
tagData = frameBuffer.readBytes(4);
final timeoutSeconds = frameBuffer.readUInt32LE();
// Start timeout timer for trace response
_timeoutTimer?.cancel();
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
});
}
// Check if it's a binary response
if (code == pushCodeTraceData && listEquals(frame.sublist(4, 8), tagData)) {
_timeoutTimer?.cancel();
if (!mounted) return;
frameBuffer.skipBytes(3); //reserved + path length + flag
if(listEquals(frameBuffer.readBytes(4), tagData)){
_handleTraceResponse(frame);
}
}
});
}
Future<void> _handleTraceResponse(Uint8List frame)async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final buffer = BufferReader(frame);
buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8();
buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code
Uint8List pathData = buffer.readBytes(pathLength);
Uint8List snrData = buffer.readRemainingBytes();
Map<int, Contact> pathContacts = {};
connector.contacts.where((c) => c.type != advTypeChat).forEach((
repeater,
) {
for (var neighbourData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([neighbourData]),
)) {
pathContacts[neighbourData] = repeater;
}
}
});
setState(() {
_isLoading = false;
_hasData = true;
_pathData = pathData;
_snrData = snrData;
_pathContacts = pathContacts;
});
}
String formatDirectionText(int index) {
if (index == 0 || index == _snrData.length - 1) {
if (index == 0) {
return context.l10n.pathTrace_you;
} else {
return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}";
}
} else {
return _pathContacts[_pathData[index-1]]?.name ?? "0x${_pathData[index-1].toRadixString(16).toUpperCase()}";
}
}
String formatDirectionSubText(int index) {
if (index == 0 || index == _snrData.length - 1) {
if (index == 0) {
return _pathContacts[_pathData[0]]?.name ?? "0x${_pathData[0].toRadixString(16).toUpperCase()}";
} else {
return context.l10n.pathTrace_you;
}
} else {
return _pathContacts[_pathData[index]]?.name ?? "0x${_pathData[index].toRadixString(16).toUpperCase()}";
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n;
return AlertDialog( return AlertDialog(
title: const Text('Path Trace'), title: Column( children: [
content: SizedBox( FittedBox(fit: BoxFit.scaleDown, child: Text(widget.title, style: const TextStyle(fontSize: 24))),
width: double.maxFinite, if(_failed2Loaded)
child: ListView.builder( Text(l10n.pathTrace_failed, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.error),),
itemCount: widget.snrData.length, ],
itemBuilder: (context, index) { ),
return ListTile( content: SafeArea(
leading: index >= widget.snrData.length / 2 ? Icon(Icons.arrow_circle_left) : Icon(Icons.arrow_circle_right), child: RefreshIndicator(
title: index == 0 || index == widget.snrData.length - 1 ? ( index == 0 ? Text('You to 0x${widget.pathData[0].toRadixString(16).toUpperCase()}') : Text('0x${widget.pathData[widget.pathData.length - 1].toRadixString(16).toUpperCase()} to You')) : Text('0x${widget.pathData[index-1].toRadixString(16).toUpperCase()} to 0x${widget.pathData[index].toRadixString(16).toUpperCase()}'), onRefresh: _doPathTrace,
trailing: SNRIcon(snr: widget.snrData[index] / 4.0), child: !_hasData
onTap: () { ? Center(
// Handle item tap child: Text(l10n.pathTrace_notAvailable),
}, )
: ListView.builder(
); itemCount: _snrData.length,
}, itemBuilder: (context, index) {
return ListTile(
leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made),
title: Text(
formatDirectionText(index), style: const TextStyle(fontSize: 14),
),
subtitle: Text(
formatDirectionSubText(index),
style: const TextStyle(fontSize: 14),
),
trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0),
onTap: () {
// Handle item tap
},
);
},
),
), ),
), ),
actions: [ actions: [
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _doPathTrace,
tooltip: l10n.pathTrace_refreshTooltip,
),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'), child: Text(l10n.common_close),
), ),
], ],
); );

Loading…
Cancel
Save

Powered by TurnKey Linux.