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