mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
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.
This commit is contained in:
parent
0ebd688787
commit
cacb9bc677
4 changed files with 247 additions and 89 deletions
|
|
@ -1308,5 +1308,24 @@
|
|||
"listFilter_repeaters": "Repeaters",
|
||||
"listFilter_roomServers": "Room servers",
|
||||
"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();
|
||||
List<ContactGroup> _groups = [];
|
||||
Timer? _searchDebounce;
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Uint8List _tagData = Uint8List(4);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadGroups();
|
||||
_setupFrameListener();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -71,44 +68,6 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
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 {
|
||||
final groups = await _groupStore.loadGroups();
|
||||
if (!mounted) return;
|
||||
|
|
@ -799,16 +758,14 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
if (isRepeater) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: Text("Ping"),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
final frame = buildTraceReq(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
0,
|
||||
0,
|
||||
payload: Uint8List.fromList([0x85,0x91,0x07,0x91,0x85]) //contact.publicKey.sublist(0,1),
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
|
||||
onTap: () {
|
||||
showDialog(context: context, builder: (context) {
|
||||
return PathTraceDialog(
|
||||
title: contact.pathLength > 0 ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing,
|
||||
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||
);
|
||||
});
|
||||
}
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -822,15 +779,14 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
]else if (isRoom) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: Text("Ping"),
|
||||
onTap: () async {
|
||||
final frame = buildTraceReq(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
0,
|
||||
0,
|
||||
payload: contact.publicKey.sublist(0,1),
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping),
|
||||
onTap: () {
|
||||
showDialog(context: context, builder: (context) {
|
||||
return PathTraceDialog(
|
||||
title: contact.pathLength > 0 ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing,
|
||||
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||
);
|
||||
});
|
||||
}
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -849,7 +805,20 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
_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(
|
||||
leading: const Icon(Icons.chat),
|
||||
title: Text(context.l10n.contacts_openChat),
|
||||
|
|
@ -869,6 +838,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
_confirmDelete(context, connector, contact);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -923,8 +893,6 @@ class _ContactTile extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shotPublicKey =
|
||||
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
|
|
@ -932,7 +900,7 @@ class _ContactTile extends StatelessWidget {
|
|||
),
|
||||
title: Text(contact.name),
|
||||
subtitle: Text(
|
||||
'${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey',
|
||||
'${contact.typeLabel} • ${contact.pathLabel} ${contact.shortPubKeyHex}',
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
|
||||
'$repeater.shortPubKeyHex',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
|
|
|||
|
|
@ -1,50 +1,221 @@
|
|||
import 'dart:async';
|
||||
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 {
|
||||
|
||||
const PathTraceDialog({
|
||||
super.key,
|
||||
required this.pathData,
|
||||
required this.snrData,
|
||||
required this.title,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
final Uint8List pathData;
|
||||
final Uint8List snrData;
|
||||
final String title;
|
||||
final Uint8List path;
|
||||
|
||||
@override
|
||||
State<PathTraceDialog> createState() => _PathTraceDialogState();
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AlertDialog(
|
||||
title: const Text('Path Trace'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
itemCount: widget.snrData.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
leading: index >= widget.snrData.length / 2 ? Icon(Icons.arrow_circle_left) : Icon(Icons.arrow_circle_right),
|
||||
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()}'),
|
||||
trailing: SNRIcon(snr: widget.snrData[index] / 4.0),
|
||||
onTap: () {
|
||||
// Handle item tap
|
||||
},
|
||||
|
||||
);
|
||||
},
|
||||
title: Column( children: [
|
||||
FittedBox(fit: BoxFit.scaleDown, child: Text(widget.title, style: const TextStyle(fontSize: 24))),
|
||||
if(_failed2Loaded)
|
||||
Text(l10n.pathTrace_failed, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.error),),
|
||||
],
|
||||
),
|
||||
content: SafeArea(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _doPathTrace,
|
||||
child: !_hasData
|
||||
? Center(
|
||||
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: [
|
||||
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(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'),
|
||||
child: Text(l10n.common_close),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue