mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-29 14:58:44 +10:00
multibyte for snr_indicator and map_filtering.
This commit is contained in:
@@ -49,15 +49,33 @@ import 'meshcore_protocol.dart';
|
|||||||
|
|
||||||
class DirectRepeater {
|
class DirectRepeater {
|
||||||
static const int maxAgeMinutes = 30; // Max age for direct repeater info
|
static const int maxAgeMinutes = 30; // Max age for direct repeater info
|
||||||
final int pubkeyFirstByte;
|
final List<int> pubkeyPrefix;
|
||||||
double snr;
|
double snr;
|
||||||
DateTime lastUpdated;
|
DateTime lastUpdated;
|
||||||
|
|
||||||
DirectRepeater({
|
DirectRepeater({
|
||||||
required this.pubkeyFirstByte,
|
required List<int> pubkeyPrefix,
|
||||||
required this.snr,
|
required this.snr,
|
||||||
DateTime? lastUpdated,
|
DateTime? lastUpdated,
|
||||||
}) : lastUpdated = lastUpdated ?? DateTime.now();
|
}) : pubkeyPrefix = List.unmodifiable(pubkeyPrefix),
|
||||||
|
lastUpdated = lastUpdated ?? DateTime.now();
|
||||||
|
|
||||||
|
String get pubkeyPrefixHex => pubkeyPrefix
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join();
|
||||||
|
|
||||||
|
bool matchesPrefix(List<int> prefix) {
|
||||||
|
return pubkeyPrefix.length == prefix.length &&
|
||||||
|
listEquals(pubkeyPrefix, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matchesPathStart(List<int> pathBytes, int hashByteWidth) {
|
||||||
|
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||||
|
if (pathBytes.length < width || pubkeyPrefix.length != width) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return listEquals(pubkeyPrefix, pathBytes.sublist(0, width));
|
||||||
|
}
|
||||||
|
|
||||||
void update(double newSNR) {
|
void update(double newSNR) {
|
||||||
snr = newSNR;
|
snr = newSNR;
|
||||||
@@ -3853,12 +3871,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_clientRepeat = frame[80] != 0;
|
_clientRepeat = frame[80] != 0;
|
||||||
}
|
}
|
||||||
// Path hash mode v10+ (byte 81): width = mode + 1 byte(s) per hop
|
// Path hash mode v10+ (byte 81): width = mode + 1 byte(s) per hop
|
||||||
|
final previousPathHashByteWidth = _pathHashByteWidth;
|
||||||
if (frame.length >= 82) {
|
if (frame.length >= 82) {
|
||||||
final mode = (frame[81] & 0xFF).clamp(0, 3);
|
final mode = (frame[81] & 0xFF).clamp(0, 3);
|
||||||
_pathHashByteWidth = mode + 1;
|
_pathHashByteWidth = mode + 1;
|
||||||
} else {
|
} else {
|
||||||
_pathHashByteWidth = 1;
|
_pathHashByteWidth = 1;
|
||||||
}
|
}
|
||||||
|
if (_pathHashByteWidth != previousPathHashByteWidth) {
|
||||||
|
_directRepeaters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
||||||
final reportedContacts = frame[2];
|
final reportedContacts = frame[2];
|
||||||
@@ -4298,7 +4320,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
for (var i = 0; i < _contacts.length; i++) {
|
for (var i = 0; i < _contacts.length; i++) {
|
||||||
final contact = _contacts[i];
|
final contact = _contacts[i];
|
||||||
if (contact.type != advTypeChat) continue;
|
if (contact.type != advTypeChat) continue;
|
||||||
if (_pathMatchesContact(pathBytes, contact.publicKey, pathHashWidth: pathHashWidth)) {
|
if (_pathMatchesContact(
|
||||||
|
pathBytes,
|
||||||
|
contact.publicKey,
|
||||||
|
pathHashWidth: pathHashWidth,
|
||||||
|
)) {
|
||||||
matches.add(i);
|
matches.add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4315,7 +4341,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _pathMatchesContact(Uint8List pathBytes, Uint8List publicKey, {int? pathHashWidth}) {
|
bool _pathMatchesContact(
|
||||||
|
Uint8List pathBytes,
|
||||||
|
Uint8List publicKey, {
|
||||||
|
int? pathHashWidth,
|
||||||
|
}) {
|
||||||
final w = pathHashWidth ?? _pathHashByteWidth;
|
final w = pathHashWidth ?? _pathHashByteWidth;
|
||||||
if (pathBytes.isEmpty || publicKey.length < w) return false;
|
if (pathBytes.isEmpty || publicKey.length < w) return false;
|
||||||
for (int i = 0; i + w <= pathBytes.length; i += w) {
|
for (int i = 0; i + w <= pathBytes.length; i += w) {
|
||||||
@@ -6170,9 +6200,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateDirectRepeater(Contact contact, double snr, Uint8List path) {
|
void _updateDirectRepeater(Contact contact, double snr, Uint8List path) {
|
||||||
final pubkeyFirstByte = path.isNotEmpty
|
final hashWidth = _pathHashByteWidth.clamp(1, 4).toInt();
|
||||||
? path.last
|
final pubkeyPrefix = path.isNotEmpty
|
||||||
: contact.publicKey.first;
|
? path.sublist(math.max(0, path.length - hashWidth))
|
||||||
|
: contact.publicKey.sublist(
|
||||||
|
0,
|
||||||
|
math.min(hashWidth, contact.publicKey.length),
|
||||||
|
);
|
||||||
|
|
||||||
_directRepeaters.removeWhere((r) => r.isStale());
|
_directRepeaters.removeWhere((r) => r.isStale());
|
||||||
|
|
||||||
@@ -6184,7 +6218,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final isTracked = _directRepeaters.where(
|
final isTracked = _directRepeaters.where(
|
||||||
(r) => r.pubkeyFirstByte == pubkeyFirstByte,
|
(r) => r.matchesPrefix(pubkeyPrefix),
|
||||||
);
|
);
|
||||||
|
|
||||||
final sortedRepeaters = List<DirectRepeater>.from(_directRepeaters)
|
final sortedRepeaters = List<DirectRepeater>.from(_directRepeaters)
|
||||||
@@ -6204,7 +6238,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
repeater.update(snr);
|
repeater.update(snr);
|
||||||
} else if (_directRepeaters.length < 5) {
|
} else if (_directRepeaters.length < 5) {
|
||||||
_directRepeaters.add(
|
_directRepeaters.add(
|
||||||
DirectRepeater(pubkeyFirstByte: pubkeyFirstByte, snr: snr),
|
DirectRepeater(pubkeyPrefix: pubkeyPrefix, snr: snr),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
@@ -17,7 +15,10 @@ class PathHelper {
|
|||||||
.join();
|
.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Uint8List> splitPathBytes(List<int> pathBytes, int hashByteWidth) {
|
static List<Uint8List> splitPathBytes(
|
||||||
|
List<int> pathBytes,
|
||||||
|
int hashByteWidth,
|
||||||
|
) {
|
||||||
if (pathBytes.isEmpty) return const [];
|
if (pathBytes.isEmpty) return const [];
|
||||||
|
|
||||||
final width = hashByteWidth.clamp(1, 4);
|
final width = hashByteWidth.clamp(1, 4);
|
||||||
@@ -51,17 +52,12 @@ class PathHelper {
|
|||||||
final hex = formatHopHex(hopBytes);
|
final hex = formatHopHex(hopBytes);
|
||||||
|
|
||||||
// Find matching contacts by comparing public key prefix
|
// Find matching contacts by comparing public key prefix
|
||||||
final matches = allContacts
|
final matches = allContacts.where((c) {
|
||||||
.where((c) {
|
if (c.publicKey.length < hopBytes.length) return false;
|
||||||
if (c.publicKey.length < hopBytes.length) return false;
|
if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
|
||||||
if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
|
// Compare bytes using listEquals for multi-byte support
|
||||||
// Compare bytes using listEquals for multi-byte support
|
return listEquals(c.publicKey.sublist(0, hopBytes.length), hopBytes);
|
||||||
return listEquals(
|
}).toList();
|
||||||
c.publicKey.sublist(0, hopBytes.length),
|
|
||||||
hopBytes,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (matches.isEmpty) {
|
if (matches.isEmpty) {
|
||||||
parts.add(hex);
|
parts.add(hex);
|
||||||
|
|||||||
@@ -805,16 +805,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
pathsWithRepeaters = paths.map((path) {
|
pathsWithRepeaters = paths.map((path) {
|
||||||
final isDirectRepeater =
|
final isDirectRepeater =
|
||||||
directRepeater != null &&
|
directRepeater != null &&
|
||||||
path.pathBytes.isNotEmpty &&
|
directRepeater.matchesPathStart(
|
||||||
directRepeater.pubkeyFirstByte == path.pathBytes.first;
|
path.pathBytes,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
final isSecondDirectRepeater =
|
final isSecondDirectRepeater =
|
||||||
secondDirectRepeater != null &&
|
secondDirectRepeater != null &&
|
||||||
path.pathBytes.isNotEmpty &&
|
secondDirectRepeater.matchesPathStart(
|
||||||
secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
path.pathBytes,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
final isThirdDirectRepeater =
|
final isThirdDirectRepeater =
|
||||||
thirdDirectRepeater != null &&
|
thirdDirectRepeater != null &&
|
||||||
path.pathBytes.isNotEmpty &&
|
thirdDirectRepeater.matchesPathStart(
|
||||||
thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
path.pathBytes,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
|
|
||||||
int ranking = -1;
|
int ranking = -1;
|
||||||
Color color = Colors.grey;
|
Color color = Colors.grey;
|
||||||
|
|||||||
@@ -1266,7 +1266,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPing,
|
title: context.l10n.contacts_repeaterPing,
|
||||||
path: Uint8List.fromList([contact.publicKey.first]),
|
path: _contactPathPrefix(contact, hw),
|
||||||
targetContact: contact,
|
targetContact: contact,
|
||||||
pathHashByteWidth: hw,
|
pathHashByteWidth: hw,
|
||||||
),
|
),
|
||||||
@@ -1299,7 +1299,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
: context.l10n.contacts_roomPing,
|
: context.l10n.contacts_roomPing,
|
||||||
path: contact.pathBytesForDisplay.isNotEmpty
|
path: contact.pathBytesForDisplay.isNotEmpty
|
||||||
? contact.pathBytesForDisplay
|
? contact.pathBytesForDisplay
|
||||||
: Uint8List.fromList([contact.publicKey.first]),
|
: _contactPathPrefix(contact, hw),
|
||||||
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
|
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
|
||||||
targetContact: contact,
|
targetContact: contact,
|
||||||
pathHashByteWidth: hw,
|
pathHashByteWidth: hw,
|
||||||
@@ -1416,6 +1416,16 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint8List _contactPathPrefix(Contact contact, int hashByteWidth) {
|
||||||
|
if (contact.publicKey.isEmpty) return Uint8List(0);
|
||||||
|
final width = hashByteWidth
|
||||||
|
.clamp(1, pubKeySize)
|
||||||
|
.toInt()
|
||||||
|
.clamp(1, contact.publicKey.length)
|
||||||
|
.toInt();
|
||||||
|
return Uint8List.fromList(contact.publicKey.sublist(0, width));
|
||||||
|
}
|
||||||
|
|
||||||
void _confirmDelete(
|
void _confirmDelete(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
|||||||
@@ -728,7 +728,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
for (final repeater in withLocation) {
|
for (final repeater in withLocation) {
|
||||||
if (repeater.type != advTypeRepeater) continue;
|
if (repeater.type != advTypeRepeater) continue;
|
||||||
if (repeater.publicKey.length < lastHop.length) continue;
|
if (repeater.publicKey.length < lastHop.length) continue;
|
||||||
if (!listEquals(repeater.publicKey.sublist(0, lastHop.length), lastHop)) {
|
if (!listEquals(
|
||||||
|
repeater.publicKey.sublist(0, lastHop.length),
|
||||||
|
lastHop,
|
||||||
|
)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!));
|
anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!));
|
||||||
@@ -986,11 +989,21 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (settings.mapShowOverlaps) {
|
if (settings.mapShowOverlaps) {
|
||||||
|
final hopWidth = context
|
||||||
|
.read<MeshCoreConnector>()
|
||||||
|
.pathHashByteWidth
|
||||||
|
.clamp(1, pubKeySize)
|
||||||
|
.toInt();
|
||||||
final hasOverlap = contacts
|
final hasOverlap = contacts
|
||||||
.where(
|
.where(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.publicKeyHex != contact.publicKeyHex &&
|
c.publicKeyHex != contact.publicKeyHex &&
|
||||||
c.publicKey.first == contact.publicKey.first &&
|
c.publicKey.length >= hopWidth &&
|
||||||
|
contact.publicKey.length >= hopWidth &&
|
||||||
|
listEquals(
|
||||||
|
c.publicKey.sublist(0, hopWidth),
|
||||||
|
contact.publicKey.sublist(0, hopWidth),
|
||||||
|
) &&
|
||||||
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||||
(contact.type == advTypeRepeater ||
|
(contact.type == advTypeRepeater ||
|
||||||
contact.type == advTypeRoom),
|
contact.type == advTypeRoom),
|
||||||
|
|||||||
@@ -202,16 +202,22 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
|||||||
paths.map((path) {
|
paths.map((path) {
|
||||||
final isDirectRepeater =
|
final isDirectRepeater =
|
||||||
directRepeater != null &&
|
directRepeater != null &&
|
||||||
path.pathBytes.isNotEmpty &&
|
directRepeater.matchesPathStart(
|
||||||
directRepeater.pubkeyFirstByte == path.pathBytes.first;
|
path.pathBytes,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
final isSecondDirectRepeater =
|
final isSecondDirectRepeater =
|
||||||
secondDirectRepeater != null &&
|
secondDirectRepeater != null &&
|
||||||
path.pathBytes.isNotEmpty &&
|
secondDirectRepeater.matchesPathStart(
|
||||||
secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
path.pathBytes,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
final isThirdDirectRepeater =
|
final isThirdDirectRepeater =
|
||||||
thirdDirectRepeater != null &&
|
thirdDirectRepeater != null &&
|
||||||
path.pathBytes.isNotEmpty &&
|
thirdDirectRepeater.matchesPathStart(
|
||||||
thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first;
|
path.pathBytes,
|
||||||
|
connector.pathHashByteWidth,
|
||||||
|
);
|
||||||
|
|
||||||
int ranking = -1;
|
int ranking = -1;
|
||||||
Color color = Colors.grey;
|
Color color = Colors.grey;
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../helpers/path_helper.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import 'signal_ui.dart';
|
import 'signal_ui.dart';
|
||||||
|
|
||||||
Contact? _getRepeaterPrefixMatchNearLocation(
|
Contact? _getRepeaterPrefixMatchNearLocation(
|
||||||
List<Contact> contacts,
|
List<Contact> contacts,
|
||||||
int pubkeyFirstByte, {
|
List<int> pubkeyPrefix, {
|
||||||
LatLng? searchPoint,
|
LatLng? searchPoint,
|
||||||
bool preferFavorites = false,
|
bool preferFavorites = false,
|
||||||
}) {
|
}) {
|
||||||
final candidates = contacts
|
final candidates = contacts
|
||||||
.where(
|
.where(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.publicKey.isNotEmpty &&
|
c.publicKey.length >= pubkeyPrefix.length &&
|
||||||
c.publicKey.first == pubkeyFirstByte &&
|
listEquals(
|
||||||
|
c.publicKey.sublist(0, pubkeyPrefix.length),
|
||||||
|
pubkeyPrefix,
|
||||||
|
) &&
|
||||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -162,7 +167,7 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
|||||||
),
|
),
|
||||||
if (directRepeater != null)
|
if (directRepeater != null)
|
||||||
Text(
|
Text(
|
||||||
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
|
'${directRepeaters.length}: ${directRepeater.pubkeyPrefixHex}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -236,23 +241,21 @@ class _SNRIndicatorState extends State<SNRIndicator> {
|
|||||||
|
|
||||||
final contact = _getRepeaterPrefixMatchNearLocation(
|
final contact = _getRepeaterPrefixMatchNearLocation(
|
||||||
allContacts,
|
allContacts,
|
||||||
repeater.pubkeyFirstByte,
|
repeater.pubkeyPrefix,
|
||||||
searchPoint: selfPoint,
|
searchPoint: selfPoint,
|
||||||
preferFavorites: true,
|
preferFavorites: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final name = contact?.name;
|
final name = contact?.name;
|
||||||
|
final prefixLabel = PathHelper.formatHopHex(
|
||||||
|
repeater.pubkeyPrefix,
|
||||||
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(snrUi.icon, color: snrUi.color),
|
leading: Icon(snrUi.icon, color: snrUi.color),
|
||||||
title: Text(
|
title: Text(name ?? prefixLabel),
|
||||||
name ??
|
|
||||||
repeater.pubkeyFirstByte
|
|
||||||
.toRadixString(16)
|
|
||||||
.padLeft(2, '0'),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}',
|
'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}',
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user