mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-15 07:04:26 +10:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a06c36ec4 | |||
| 302589f9f4 | |||
| b8acedd03e | |||
| 17a9db0f0e | |||
| 0a01ecde38 | |||
| 14cec533ac | |||
| fdfc1f6d25 | |||
| 42eb293d1c | |||
| 36401210ce | |||
| a68e1dd428 | |||
| 52a578777d | |||
| 71152bd3eb | |||
| 940a1be203 | |||
| 75a7f437f6 | |||
| e6814b4f48 | |||
| 1589883c88 | |||
| 03b3533675 | |||
| 608b6fb539 | |||
| 04f5c44ed9 | |||
| 4019741a81 | |||
| 152d5f8bb5 | |||
| 3f80ae1cf7 | |||
| 246cf99415 | |||
| 5751cddaa1 | |||
| fa5a0932ee | |||
| cdda232006 | |||
| 63aa515f52 | |||
| aed3b0157a |
@@ -37,6 +37,42 @@ class MeshCoreUuids {
|
|||||||
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DirectRepeater {
|
||||||
|
static const int maxAgeMinutes = 30; // Max age for direct repeater info
|
||||||
|
final int pubkeyFirstByte;
|
||||||
|
double snr;
|
||||||
|
DateTime lastUpdated;
|
||||||
|
|
||||||
|
DirectRepeater({
|
||||||
|
required this.pubkeyFirstByte,
|
||||||
|
required this.snr,
|
||||||
|
DateTime? lastUpdated,
|
||||||
|
}) : lastUpdated = lastUpdated ?? DateTime.now();
|
||||||
|
|
||||||
|
void update(double newSNR) {
|
||||||
|
snr = newSNR;
|
||||||
|
lastUpdated = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
int get ranking {
|
||||||
|
if (isStale()) {
|
||||||
|
return -1; // Stale repeaters get lowest rank
|
||||||
|
}
|
||||||
|
// Higher SNR gets higher rank and recency within maxAgeMinutes breaks ties.
|
||||||
|
final ageMs =
|
||||||
|
DateTime.now().millisecondsSinceEpoch -
|
||||||
|
lastUpdated.millisecondsSinceEpoch;
|
||||||
|
final maxAgeMs = maxAgeMinutes * 60 * 1000;
|
||||||
|
final recencyScore = (maxAgeMs - ageMs).clamp(0, maxAgeMs);
|
||||||
|
return ((snr - 31.75) * 1000).round() + recencyScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isStale() {
|
||||||
|
return DateTime.now().difference(lastUpdated) >
|
||||||
|
const Duration(minutes: maxAgeMinutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum MeshCoreConnectionState {
|
enum MeshCoreConnectionState {
|
||||||
disconnected,
|
disconnected,
|
||||||
scanning,
|
scanning,
|
||||||
@@ -93,6 +129,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int? _batteryMillivolts;
|
int? _batteryMillivolts;
|
||||||
double? _selfLatitude;
|
double? _selfLatitude;
|
||||||
double? _selfLongitude;
|
double? _selfLongitude;
|
||||||
|
final List<DirectRepeater> _directRepeaters = List.empty(growable: true);
|
||||||
bool _isLoadingContacts = false;
|
bool _isLoadingContacts = false;
|
||||||
bool _isLoadingChannels = false;
|
bool _isLoadingChannels = false;
|
||||||
bool _hasLoadedChannels = false;
|
bool _hasLoadedChannels = false;
|
||||||
@@ -194,6 +231,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
String? get selfName => _selfName;
|
String? get selfName => _selfName;
|
||||||
double? get selfLatitude => _selfLatitude;
|
double? get selfLatitude => _selfLatitude;
|
||||||
double? get selfLongitude => _selfLongitude;
|
double? get selfLongitude => _selfLongitude;
|
||||||
|
List<DirectRepeater> get directRepeaters => _directRepeaters;
|
||||||
int? get currentTxPower => _currentTxPower;
|
int? get currentTxPower => _currentTxPower;
|
||||||
int? get maxTxPower => _maxTxPower;
|
int? get maxTxPower => _maxTxPower;
|
||||||
int? get currentFreqHz => _currentFreqHz;
|
int? get currentFreqHz => _currentFreqHz;
|
||||||
@@ -1690,6 +1728,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_isLoadingContacts = true;
|
_isLoadingContacts = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
break;
|
||||||
|
case pushCodeNewAdvert:
|
||||||
|
debugPrint('Got New CONTACT');
|
||||||
|
// It's the same format as respCodeContact, so we can reuse the handler
|
||||||
|
_handleContact(frame);
|
||||||
|
break;
|
||||||
case respCodeContact:
|
case respCodeContact:
|
||||||
debugPrint('Got CONTACT');
|
debugPrint('Got CONTACT');
|
||||||
_handleContact(frame);
|
_handleContact(frame);
|
||||||
@@ -1734,6 +1777,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
case pushCodeStatusResponse:
|
case pushCodeStatusResponse:
|
||||||
break;
|
break;
|
||||||
case pushCodeLogRxData:
|
case pushCodeLogRxData:
|
||||||
|
_handleRxData(frame);
|
||||||
_handleLogRxData(frame);
|
_handleLogRxData(frame);
|
||||||
break;
|
break;
|
||||||
case respCodeChannelInfo:
|
case respCodeChannelInfo:
|
||||||
@@ -1747,6 +1791,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
case respCodeCustomVars:
|
case respCodeCustomVars:
|
||||||
_handleCustomVars(frame);
|
_handleCustomVars(frame);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
debugPrint('Unknown frame code: $code');
|
debugPrint('Unknown frame code: $code');
|
||||||
}
|
}
|
||||||
@@ -2002,6 +2047,80 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleContactAdvert(Contact contact) {
|
||||||
|
if (listEquals(contact.publicKey, _selfPublicKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact.type == advTypeRepeater) {
|
||||||
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||||
|
_unreadStore.saveContactUnreadCount(
|
||||||
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Check if this is a new contact
|
||||||
|
final isNewContact = !_knownContactKeys.contains(contact.publicKeyHex);
|
||||||
|
final existingIndex = _contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
final existing = _contacts[existingIndex];
|
||||||
|
final mergedLastMessageAt =
|
||||||
|
existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
||||||
|
? existing.lastMessageAt
|
||||||
|
: contact.lastMessageAt;
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
|
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||||
|
_contacts[existingIndex] = contact.copyWith(
|
||||||
|
lastMessageAt: mergedLastMessageAt,
|
||||||
|
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||||
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_contacts.add(contact);
|
||||||
|
appLogger.info(
|
||||||
|
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_knownContactKeys.add(contact.publicKeyHex);
|
||||||
|
_loadMessagesForContact(contact.publicKeyHex);
|
||||||
|
|
||||||
|
// Add path to history if we have a valid path
|
||||||
|
if (_pathHistoryService != null && contact.pathLength >= 0) {
|
||||||
|
_pathHistoryService!.handlePathUpdated(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Show notification for new contact (advertisement)
|
||||||
|
if (isNewContact && _appSettingsService != null) {
|
||||||
|
final settings = _appSettingsService!.settings;
|
||||||
|
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||||
|
_notificationService.showAdvertNotification(
|
||||||
|
contactName: contact.name,
|
||||||
|
contactType: contact.typeLabel,
|
||||||
|
contactId: contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isLoadingContacts) {
|
||||||
|
unawaited(_persistContacts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _persistContacts() async {
|
Future<void> _persistContacts() async {
|
||||||
await _contactStore.saveContacts(_contacts);
|
await _contactStore.saveContacts(_contacts);
|
||||||
}
|
}
|
||||||
@@ -3261,7 +3380,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
void _handleCustomVars(Uint8List frame) {
|
void _handleCustomVars(Uint8List frame) {
|
||||||
final buf = BufferReader(frame.sublist(1));
|
final buf = BufferReader(frame.sublist(1));
|
||||||
|
try {
|
||||||
_currentCustomVars = _parseKeyValueString(buf.readString());
|
_currentCustomVars = _parseKeyValueString(buf.readString());
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setState(MeshCoreConnectionState newState) {
|
void _setState(MeshCoreConnectionState newState) {
|
||||||
@@ -3285,6 +3408,173 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleRxData(Uint8List frame) {
|
||||||
|
final packet = BufferReader(frame);
|
||||||
|
try {
|
||||||
|
packet.skipBytes(1); // Skip frame type byte
|
||||||
|
final snr = packet.readInt8() / 4.0;
|
||||||
|
packet.skipBytes(1); // Skip RSSI byte
|
||||||
|
//final rssi = packet.readByte();
|
||||||
|
final header = packet.readByte();
|
||||||
|
final routeType = header & 0x03;
|
||||||
|
final payloadType = (header >> 2) & 0x0F;
|
||||||
|
//final payloadVer = (header >> 6) & 0x03;
|
||||||
|
final pathLen = packet.readByte();
|
||||||
|
final pathBytes = packet.readBytes(pathLen);
|
||||||
|
final payload = packet.readBytes(packet.remaining);
|
||||||
|
|
||||||
|
switch (payloadType) {
|
||||||
|
case payloadTypeADVERT:
|
||||||
|
_handlePayloadAdvertReceived(payload, pathBytes, routeType, snr);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePayloadAdvertReceived(
|
||||||
|
Uint8List frame,
|
||||||
|
Uint8List path,
|
||||||
|
int routeType,
|
||||||
|
double snr,
|
||||||
|
) {
|
||||||
|
final advert = BufferReader(frame);
|
||||||
|
double latitude = 0.0;
|
||||||
|
double longitude = 0.0;
|
||||||
|
String name = '';
|
||||||
|
String contactKeyHex = '';
|
||||||
|
Uint8List publicKey = Uint8List(0);
|
||||||
|
int type = 0;
|
||||||
|
int timestamp = 0;
|
||||||
|
bool hasLocation = false;
|
||||||
|
bool hasName = false;
|
||||||
|
try {
|
||||||
|
publicKey = advert.readBytes(32);
|
||||||
|
contactKeyHex = publicKey
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
|
||||||
|
timestamp = advert.readInt32LE();
|
||||||
|
//TODO add signature verification
|
||||||
|
advert.skipBytes(64); // Skip signature for now
|
||||||
|
final flags = advert.readByte();
|
||||||
|
type = flags & 0x0F;
|
||||||
|
hasLocation = (flags & 0x10) != 0;
|
||||||
|
// For future use:
|
||||||
|
//final hasFeature1 = (flags & 0x20) != 0;
|
||||||
|
//final hasFeature2 = (flags & 0x40) != 0;
|
||||||
|
hasName = (flags & 0x80) != 0;
|
||||||
|
if (hasLocation && advert.remaining >= 8) {
|
||||||
|
latitude = advert.readInt32LE() / 1e6;
|
||||||
|
longitude = advert.readInt32LE() / 1e6;
|
||||||
|
}
|
||||||
|
if (hasName && advert.remaining > 0) {
|
||||||
|
name = advert.readString();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listEquals(publicKey, _selfPublicKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a new contact
|
||||||
|
final isNewContact = !_knownContactKeys.contains(contactKeyHex);
|
||||||
|
|
||||||
|
if (isNewContact) {
|
||||||
|
final newContact = Contact(
|
||||||
|
publicKey: publicKey,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
pathLength: path.length,
|
||||||
|
path: Uint8List.fromList(
|
||||||
|
path.reversed.toList(),
|
||||||
|
), // Store path in reverse for easier use in outgoing messages
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||||
|
);
|
||||||
|
_handleContactAdvert(newContact);
|
||||||
|
_updateDirectRepeater(newContact, snr, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingIndex = _contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contactKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
final existing = _contacts[existingIndex];
|
||||||
|
final mergedLastMessageAt = existing.lastMessageAt.isAfter(DateTime.now())
|
||||||
|
? DateTime.now()
|
||||||
|
: existing.lastMessageAt;
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
|
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||||
|
_contacts[existingIndex] = existing.copyWith(
|
||||||
|
latitude: hasLocation ? latitude : existing.latitude,
|
||||||
|
longitude: hasLocation ? longitude : existing.longitude,
|
||||||
|
name: hasName ? name : existing.name,
|
||||||
|
path: Uint8List.fromList(path.reversed.toList()),
|
||||||
|
pathLength: path.length,
|
||||||
|
lastMessageAt: mergedLastMessageAt,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||||
|
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||||
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add path to history if we have a valid path
|
||||||
|
if (_pathHistoryService != null &&
|
||||||
|
_contacts[existingIndex].pathLength >= 0) {
|
||||||
|
_pathHistoryService!.handlePathUpdated(_contacts[existingIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateDirectRepeater(_contacts[existingIndex], snr, path);
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateDirectRepeater(Contact contact, double snr, Uint8List path) {
|
||||||
|
final pubkeyFirstByte = path.isNotEmpty
|
||||||
|
? path.last
|
||||||
|
: contact.publicKey.first;
|
||||||
|
|
||||||
|
_directRepeaters.removeWhere((r) => r.isStale());
|
||||||
|
|
||||||
|
//We can use adverts from chat and sensor nodes, but only if the advert has a path to get the last hop.
|
||||||
|
if ((contact.type == advTypeChat || contact.type == advTypeSensor) &&
|
||||||
|
path.isEmpty) {
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isTracked = _directRepeaters.where(
|
||||||
|
(r) => r.pubkeyFirstByte == pubkeyFirstByte,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTracked.isNotEmpty) {
|
||||||
|
final repeater = isTracked.first;
|
||||||
|
repeater.update(snr);
|
||||||
|
} else if (_directRepeaters.length < 5) {
|
||||||
|
_directRepeaters.add(
|
||||||
|
DirectRepeater(pubkeyFirstByte: pubkeyFirstByte, snr: snr),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const int _phRouteMask = 0x03;
|
const int _phRouteMask = 0x03;
|
||||||
|
|||||||
@@ -13,12 +13,22 @@ class BufferReader {
|
|||||||
int readByte() => readBytes(1)[0];
|
int readByte() => readBytes(1)[0];
|
||||||
|
|
||||||
Uint8List readBytes(int count) {
|
Uint8List readBytes(int count) {
|
||||||
|
if (_pointer + count > _buffer.length) {
|
||||||
|
throw RangeError(
|
||||||
|
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
final data = _buffer.sublist(_pointer, _pointer + count);
|
final data = _buffer.sublist(_pointer, _pointer + count);
|
||||||
_pointer += count;
|
_pointer += count;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void skipBytes(int count) {
|
void skipBytes(int count) {
|
||||||
|
if (_pointer + count > _buffer.length) {
|
||||||
|
throw RangeError(
|
||||||
|
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
_pointer += count;
|
_pointer += count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +161,7 @@ const int cmdGetContactByKey = 30;
|
|||||||
const int cmdGetChannel = 31;
|
const int cmdGetChannel = 31;
|
||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdSendTracePath = 36;
|
const int cmdSendTracePath = 36;
|
||||||
|
const int cmdSetOtherParams = 38;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdGetRadioSettings = 57;
|
||||||
const int cmdGetTelemetryReq = 39;
|
const int cmdGetTelemetryReq = 39;
|
||||||
const int cmdGetCustomVar = 40;
|
const int cmdGetCustomVar = 40;
|
||||||
@@ -166,7 +177,7 @@ const int reqTypeGetStatus = 0x01;
|
|||||||
const int reqTypeKeepAlive = 0x02;
|
const int reqTypeKeepAlive = 0x02;
|
||||||
const int reqTypeGetTelemetry = 0x03;
|
const int reqTypeGetTelemetry = 0x03;
|
||||||
const int reqTypeGetAccessList = 0x05;
|
const int reqTypeGetAccessList = 0x05;
|
||||||
const int reqTypeGetNeighbours = 0x06;
|
const int reqTypeGetNeighbors = 0x06;
|
||||||
|
|
||||||
// Repeater response codes
|
// Repeater response codes
|
||||||
const int respServerLoginOk = 0;
|
const int respServerLoginOk = 0;
|
||||||
@@ -212,6 +223,30 @@ const int advTypeRepeater = 2;
|
|||||||
const int advTypeRoom = 3;
|
const int advTypeRoom = 3;
|
||||||
const int advTypeSensor = 4;
|
const int advTypeSensor = 4;
|
||||||
|
|
||||||
|
// Payload Types
|
||||||
|
const int payloadTypeREQ =
|
||||||
|
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeRESPONSE =
|
||||||
|
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeTXTMSG =
|
||||||
|
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
|
||||||
|
const int payloadTypeACK = 0x03; // a simple ack
|
||||||
|
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
|
||||||
|
const int payloadTypeGRPTXT =
|
||||||
|
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
|
||||||
|
const int payloadTypeGRPDATA =
|
||||||
|
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeANONREQ =
|
||||||
|
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||||
|
const int payloadTypePATH =
|
||||||
|
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||||
|
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
|
||||||
|
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
|
||||||
|
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||||
|
//...
|
||||||
|
const int payloadTypeRawCustom =
|
||||||
|
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||||
|
|
||||||
// Sizes
|
// Sizes
|
||||||
const int pubKeySize = 32;
|
const int pubKeySize = 32;
|
||||||
const int maxPathSize = 64;
|
const int maxPathSize = 64;
|
||||||
@@ -777,3 +812,22 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
|||||||
writer.writeBytes(pubKey);
|
writer.writeBytes(pubKey);
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build CMD_SET_OTHER_PARAMS frame
|
||||||
|
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||||
|
Uint8List buildSetOtherParamsFrame(
|
||||||
|
bool allowAutoAddContacts,
|
||||||
|
int allowTelemetryFlags,
|
||||||
|
int advertLocationPolicy,
|
||||||
|
int multiAcks,
|
||||||
|
) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetOtherParams);
|
||||||
|
writer.writeByte(
|
||||||
|
allowAutoAddContacts ? 0x00 : 0x01,
|
||||||
|
); // Allow Auto Add Contacts
|
||||||
|
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||||
|
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||||
|
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
class CayenneLpp {
|
class CayenneLpp {
|
||||||
@@ -84,7 +86,7 @@ class CayenneLpp {
|
|||||||
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
||||||
final buffer = BufferReader(bytes);
|
final buffer = BufferReader(bytes);
|
||||||
final telemetry = <Map<String, dynamic>>[];
|
final telemetry = <Map<String, dynamic>>[];
|
||||||
|
try {
|
||||||
while (buffer.remaining >= 2) {
|
while (buffer.remaining >= 2) {
|
||||||
final channel = buffer.readUInt8();
|
final channel = buffer.readUInt8();
|
||||||
final type = buffer.readUInt8();
|
final type = buffer.readUInt8();
|
||||||
@@ -186,14 +188,19 @@ class CayenneLpp {
|
|||||||
return telemetry;
|
return telemetry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return telemetry;
|
return telemetry;
|
||||||
|
} catch (e) {
|
||||||
|
// Handle parsing errors, possibly due to malformed data
|
||||||
|
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||||
|
// Return any telemetry parsed so far to preserve partial data
|
||||||
|
return telemetry;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
|
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
|
||||||
final buffer = BufferReader(bytes);
|
final buffer = BufferReader(bytes);
|
||||||
final Map<int, Map<String, dynamic>> channels = {};
|
final Map<int, Map<String, dynamic>> channels = {};
|
||||||
|
try {
|
||||||
while (buffer.remaining >= 2) {
|
while (buffer.remaining >= 2) {
|
||||||
final channel = buffer.readUInt8();
|
final channel = buffer.readUInt8();
|
||||||
final type = buffer.readUInt8();
|
final type = buffer.readUInt8();
|
||||||
@@ -251,13 +258,20 @@ class CayenneLpp {
|
|||||||
break;
|
break;
|
||||||
// Add more types as needed...
|
// Add more types as needed...
|
||||||
default:
|
default:
|
||||||
// Unknown type: skip or handle error?
|
//Stopped parsing to avoid misalignment
|
||||||
continue;
|
return channels.values.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||||
return channelsOut;
|
return channelsOut;
|
||||||
|
} catch (e) {
|
||||||
|
// Handle parsing errors, possibly due to malformed data
|
||||||
|
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||||
|
return <
|
||||||
|
Map<String, dynamic>
|
||||||
|
>[]; // Return an empty list on error to avoid crashing the app
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
|
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
|
||||||
"repeater_neighbours": "Съседи",
|
"repeater_neighbors": "Съседи",
|
||||||
"neighbors_receivedData": "Получени данни за съседи",
|
"neighbors_receivedData": "Получени данни за съседи",
|
||||||
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
||||||
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Повторители Съседи",
|
"neighbors_repeatersNeighbors": "Повторители Съседи",
|
||||||
"neighbors_noData": "Няма налични данни за съседи.",
|
"neighbors_noData": "Няма налични данни за съседи.",
|
||||||
"channels_createPrivateChannel": "Създай Частен Канал",
|
"channels_createPrivateChannel": "Създай Частен Канал",
|
||||||
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
|
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
|
||||||
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||||
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства."
|
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||||
|
"snrIndicator_lastSeen": "Последно видян",
|
||||||
|
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Nachbarn",
|
"repeater_neighbors": "Nachbarn",
|
||||||
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
"repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||||
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
||||||
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
||||||
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Nachbarn",
|
"neighbors_repeatersNeighbors": "Nachbarn",
|
||||||
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
||||||
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
||||||
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
||||||
@@ -1626,5 +1626,7 @@
|
|||||||
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
||||||
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
||||||
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
||||||
"scanner_enableBluetooth": "Bluetooth aktivieren"
|
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
||||||
|
"snrIndicator_lastSeen": "Zuletzt gesehen",
|
||||||
|
"snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater"
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-7
@@ -793,8 +793,8 @@
|
|||||||
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
|
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
|
||||||
"repeater_cli": "CLI",
|
"repeater_cli": "CLI",
|
||||||
"repeater_cliSubtitle": "Send commands to the repeater",
|
"repeater_cliSubtitle": "Send commands to the repeater",
|
||||||
"repeater_neighbours": "Neighbors",
|
"repeater_neighbors": "Neighbors",
|
||||||
"repeater_neighboursSubtitle": "View zero hop neighbors.",
|
"repeater_neighborsSubtitle": "View zero hop neighbors.",
|
||||||
"repeater_settings": "Settings",
|
"repeater_settings": "Settings",
|
||||||
"repeater_settingsSubtitle": "Configure repeater parameters",
|
"repeater_settingsSubtitle": "Configure repeater parameters",
|
||||||
|
|
||||||
@@ -1098,16 +1098,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"neighbors_receivedData": "Received Neighbours Data",
|
"neighbors_receivedData": "Received Neighbors Data",
|
||||||
"neighbors_requestTimedOut": "Neighbours request timed out.",
|
"neighbors_requestTimedOut": "Neighbors request timed out.",
|
||||||
"neighbors_errorLoading": "Error loading neighbors: {error}",
|
"neighbors_errorLoading": "Error loading neighbors: {error}",
|
||||||
"@neighbors_errorLoading": {
|
"@neighbors_errorLoading": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"error": {"type": "String"}
|
"error": {"type": "String"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"neighbors_repeatersNeighbours": "Repeaters Neighbours",
|
"neighbors_repeatersNeighbors": "Repeaters Neighbors",
|
||||||
"neighbors_noData": "No neighbours data available.",
|
"neighbors_noData": "No neighbors data available.",
|
||||||
"neighbors_unknownContact": "Unknown {pubkey}",
|
"neighbors_unknownContact": "Unknown {pubkey}",
|
||||||
"@neighbors_unknownContact": {
|
"@neighbors_unknownContact": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1395,5 +1395,7 @@
|
|||||||
"settings_gpxExportChat": "Companion locations",
|
"settings_gpxExportChat": "Companion locations",
|
||||||
"settings_gpxExportAllContacts": "All contacts locations",
|
"settings_gpxExportAllContacts": "All contacts locations",
|
||||||
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
||||||
"settings_gpxExportShareSubject": "meshcore-open GPX map data export"
|
"settings_gpxExportShareSubject": "meshcore-open GPX map data export",
|
||||||
|
"snrIndicator_nearByRepeaters": "Nearby Repeaters",
|
||||||
|
"snrIndicator_lastSeen": "Last seen"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vecinos",
|
"repeater_neighbors": "Vecinos",
|
||||||
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
|
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
|
||||||
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
||||||
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
||||||
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
|
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
|
||||||
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
||||||
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
||||||
"channels_createPrivateChannel": "Crear un Canal Privado",
|
"channels_createPrivateChannel": "Crear un Canal Privado",
|
||||||
@@ -1626,5 +1626,7 @@
|
|||||||
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
||||||
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
||||||
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
||||||
"scanner_enableBluetooth": "Habilitar Bluetooth"
|
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
||||||
|
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
|
||||||
|
"snrIndicator_lastSeen": "Visto por última vez"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Voisins",
|
"repeater_neighbors": "Voisins",
|
||||||
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
|
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
|
||||||
"neighbors_receivedData": "Données des voisins reçues",
|
"neighbors_receivedData": "Données des voisins reçues",
|
||||||
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
||||||
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
|
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
|
||||||
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
|
"neighbors_repeatersNeighbors": "Répéteurs Voisins",
|
||||||
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
|
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
|
||||||
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
||||||
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_runTrace": "Exécuter la traçage de chemin",
|
"map_runTrace": "Exécuter la traçage de chemin",
|
||||||
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
||||||
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
||||||
"scanner_enableBluetooth": "Activer le Bluetooth"
|
"scanner_enableBluetooth": "Activer le Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Dernière fois vu",
|
||||||
|
"snrIndicator_nearByRepeaters": "Répéteurs à proximité"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vicini",
|
"repeater_neighbors": "Vicini",
|
||||||
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
|
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
|
||||||
"neighbors_receivedData": "Ricevute dati vicini",
|
"neighbors_receivedData": "Ricevute dati vicini",
|
||||||
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
||||||
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
|
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
|
||||||
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
||||||
"channels_createPrivateChannel": "Crea un Canale Privato",
|
"channels_createPrivateChannel": "Crea un Canale Privato",
|
||||||
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||||
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
||||||
"scanner_enableBluetooth": "Abilita il Bluetooth"
|
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
|
||||||
|
"snrIndicator_lastSeen": "Ultimo accesso"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3039,17 +3039,17 @@ abstract class AppLocalizations {
|
|||||||
/// **'Send commands to the repeater'**
|
/// **'Send commands to the repeater'**
|
||||||
String get repeater_cliSubtitle;
|
String get repeater_cliSubtitle;
|
||||||
|
|
||||||
/// No description provided for @repeater_neighbours.
|
/// No description provided for @repeater_neighbors.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Neighbors'**
|
/// **'Neighbors'**
|
||||||
String get repeater_neighbours;
|
String get repeater_neighbors;
|
||||||
|
|
||||||
/// No description provided for @repeater_neighboursSubtitle.
|
/// No description provided for @repeater_neighborsSubtitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'View zero hop neighbors.'**
|
/// **'View zero hop neighbors.'**
|
||||||
String get repeater_neighboursSubtitle;
|
String get repeater_neighborsSubtitle;
|
||||||
|
|
||||||
/// No description provided for @repeater_settings.
|
/// No description provided for @repeater_settings.
|
||||||
///
|
///
|
||||||
@@ -4193,13 +4193,13 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @neighbors_receivedData.
|
/// No description provided for @neighbors_receivedData.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Received Neighbours Data'**
|
/// **'Received Neighbors Data'**
|
||||||
String get neighbors_receivedData;
|
String get neighbors_receivedData;
|
||||||
|
|
||||||
/// No description provided for @neighbors_requestTimedOut.
|
/// No description provided for @neighbors_requestTimedOut.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Neighbours request timed out.'**
|
/// **'Neighbors request timed out.'**
|
||||||
String get neighbors_requestTimedOut;
|
String get neighbors_requestTimedOut;
|
||||||
|
|
||||||
/// No description provided for @neighbors_errorLoading.
|
/// No description provided for @neighbors_errorLoading.
|
||||||
@@ -4208,16 +4208,16 @@ abstract class AppLocalizations {
|
|||||||
/// **'Error loading neighbors: {error}'**
|
/// **'Error loading neighbors: {error}'**
|
||||||
String neighbors_errorLoading(String error);
|
String neighbors_errorLoading(String error);
|
||||||
|
|
||||||
/// No description provided for @neighbors_repeatersNeighbours.
|
/// No description provided for @neighbors_repeatersNeighbors.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Repeaters Neighbours'**
|
/// **'Repeaters Neighbors'**
|
||||||
String get neighbors_repeatersNeighbours;
|
String get neighbors_repeatersNeighbors;
|
||||||
|
|
||||||
/// No description provided for @neighbors_noData.
|
/// No description provided for @neighbors_noData.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'No neighbours data available.'**
|
/// **'No neighbors data available.'**
|
||||||
String get neighbors_noData;
|
String get neighbors_noData;
|
||||||
|
|
||||||
/// No description provided for @neighbors_unknownContact.
|
/// No description provided for @neighbors_unknownContact.
|
||||||
@@ -5035,6 +5035,18 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'meshcore-open GPX map data export'**
|
/// **'meshcore-open GPX map data export'**
|
||||||
String get settings_gpxExportShareSubject;
|
String get settings_gpxExportShareSubject;
|
||||||
|
|
||||||
|
/// No description provided for @snrIndicator_nearByRepeaters.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Nearby Repeaters'**
|
||||||
|
String get snrIndicator_nearByRepeaters;
|
||||||
|
|
||||||
|
/// No description provided for @snrIndicator_lastSeen.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Last seen'**
|
||||||
|
String get snrIndicator_lastSeen;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -1681,10 +1681,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Съседи';
|
String get repeater_neighbors => 'Съседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Преглед на съседни възли с нулев скок.';
|
'Преглед на съседни възли с нулев скок.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2384,7 +2384,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
|
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Няма налични данни за съседи.';
|
String get neighbors_noData => 'Няма налични данни за съседи.';
|
||||||
@@ -2894,4 +2894,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open износ на данни за карта в формат GPX';
|
'meshcore-open износ на данни за карта в формат GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Последно видян';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1680,10 +1680,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
|
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Nachbarn';
|
String get repeater_neighbors => 'Nachbarn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
|
String get repeater_neighborsSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Einstellungen';
|
String get repeater_settings => 'Einstellungen';
|
||||||
@@ -2386,7 +2386,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Nachbarn';
|
String get neighbors_repeatersNeighbors => 'Nachbarn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
|
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
|
||||||
@@ -2902,4 +2902,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'GPX-Kartendaten aus meshcore-open exportieren';
|
'GPX-Kartendaten aus meshcore-open exportieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1654,10 +1654,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Send commands to the repeater';
|
String get repeater_cliSubtitle => 'Send commands to the repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Neighbors';
|
String get repeater_neighbors => 'Neighbors';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
|
String get repeater_neighborsSubtitle => 'View zero hop neighbors.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Settings';
|
String get repeater_settings => 'Settings';
|
||||||
@@ -2333,10 +2333,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_receivedData => 'Received Neighbours Data';
|
String get neighbors_receivedData => 'Received Neighbors Data';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
|
String get neighbors_requestTimedOut => 'Neighbors request timed out.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String neighbors_errorLoading(String error) {
|
String neighbors_errorLoading(String error) {
|
||||||
@@ -2344,10 +2344,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
|
String get neighbors_repeatersNeighbors => 'Repeaters Neighbors';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'No neighbours data available.';
|
String get neighbors_noData => 'No neighbors data available.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String neighbors_unknownContact(String pubkey) {
|
String neighbors_unknownContact(String pubkey) {
|
||||||
@@ -2849,4 +2849,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open GPX map data export';
|
'meshcore-open GPX map data export';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Nearby Repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Last seen';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1678,10 +1678,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
|
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vecinos';
|
String get repeater_neighbors => 'Vecinos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
|
String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Configuración';
|
String get repeater_settings => 'Configuración';
|
||||||
@@ -2380,7 +2380,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
|
String get neighbors_repeatersNeighbors => 'Repetidores Vecinos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
|
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
|
||||||
@@ -2893,4 +2893,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open exportación de datos de mapa GPX';
|
'meshcore-open exportación de datos de mapa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Repetidores cercanos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Visto por última vez';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1686,11 +1686,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
|
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Voisins';
|
String get repeater_neighbors => 'Voisins';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.';
|
||||||
'Afficher les voisins de saut nuls.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Paramètres';
|
String get repeater_settings => 'Paramètres';
|
||||||
@@ -2395,7 +2394,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
|
String get neighbors_repeatersNeighbors => 'Répéteurs Voisins';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData =>
|
String get neighbors_noData =>
|
||||||
@@ -2917,4 +2916,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open exporter les données de carte GPX';
|
'meshcore-open exporter les données de carte GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Dernière fois vu';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1676,10 +1676,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vicini';
|
String get repeater_neighbors => 'Vicini';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Visualizza vicini di salto pari a zero.';
|
'Visualizza vicini di salto pari a zero.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2380,7 +2380,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
|
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
||||||
@@ -2897,4 +2897,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open esportazione dati mappa GPX';
|
'meshcore-open esportazione dati mappa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1672,10 +1672,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
|
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Buren';
|
String get repeater_neighbors => 'Buren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
|
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Instellingen';
|
String get repeater_settings => 'Instellingen';
|
||||||
@@ -2371,7 +2371,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
|
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
||||||
@@ -2885,4 +2885,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open GPX kaartgegevens exporteren';
|
'meshcore-open GPX kaartgegevens exporteren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Laatst gezien';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1680,10 +1680,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
|
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Sąsiedzi';
|
String get repeater_neighbors => 'Sąsiedzi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Wyświetl sąsiedztwo zerowych hopów.';
|
'Wyświetl sąsiedztwo zerowych hopów.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2379,7 +2379,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
|
String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
|
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
|
||||||
@@ -2899,4 +2899,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'Eksport danych mapy GPX meshcore-open';
|
'Eksport danych mapy GPX meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Ostatnio widziany';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1678,11 +1678,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vizinhos';
|
String get repeater_neighbors => 'Vizinhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
|
||||||
'Visualizar vizinhos de salto zero.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Configurações';
|
String get repeater_settings => 'Configurações';
|
||||||
@@ -2381,7 +2380,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
|
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
||||||
@@ -2894,4 +2893,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open exportação de dados de mapa GPX';
|
'meshcore-open exportação de dados de mapa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Repetidores Próximos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Visto pela última vez';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1680,10 +1680,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Соседи';
|
String get repeater_neighbors => 'Соседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.';
|
String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Настройки';
|
String get repeater_settings => 'Настройки';
|
||||||
@@ -2383,7 +2383,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Соседи репитеров';
|
String get neighbors_repeatersNeighbors => 'Соседи репитеров';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Данные о соседях недоступны.';
|
String get neighbors_noData => 'Данные о соседях недоступны.';
|
||||||
@@ -2905,4 +2905,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open экспорт данных карты GPX';
|
'meshcore-open экспорт данных карты GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Последний раз видели';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1673,10 +1673,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Súsezný';
|
String get repeater_neighbors => 'Súsezný';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
|
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Nastavenia';
|
String get repeater_settings => 'Nastavenia';
|
||||||
@@ -2367,7 +2367,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
|
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData =>
|
String get neighbors_noData =>
|
||||||
@@ -2881,4 +2881,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open export dát GPX mapových údajov';
|
'meshcore-open export dát GPX mapových údajov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Miestne opakovače';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Naposledy videný';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1672,10 +1672,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
'Pošlji ukazne povelje na ponovitveno enoto.';
|
'Pošlji ukazne povelje na ponovitveno enoto.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Sosedi';
|
String get repeater_neighbors => 'Sosedi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
|
String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Nastavitve';
|
String get repeater_settings => 'Nastavitve';
|
||||||
@@ -2371,7 +2371,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
|
String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
|
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
|
||||||
@@ -2886,4 +2886,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open izvoz podatkov GPX karte';
|
'meshcore-open izvoz podatkov GPX karte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Zadnjič videno';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1662,10 +1662,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Grannar';
|
String get repeater_neighbors => 'Grannar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
|
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Inställningar';
|
String get repeater_settings => 'Inställningar';
|
||||||
@@ -2356,7 +2356,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
|
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
||||||
@@ -2866,4 +2866,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open export av GPX-kartdata';
|
'meshcore-open export av GPX-kartdata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Senast sedd';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1679,10 +1679,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
|
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Сусіди';
|
String get repeater_neighbors => 'Сусіди';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Показати сусідів нульового стрибка.';
|
'Показати сусідів нульового стрибка.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2384,7 +2384,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди';
|
String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Дані про сусідів недоступні.';
|
String get neighbors_noData => 'Дані про сусідів недоступні.';
|
||||||
@@ -2911,4 +2911,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'експорт даних карти meshcore-open у форматі GPX';
|
'експорт даних карти meshcore-open у форматі GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Останній раз бачили';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1601,10 +1601,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => '向复用器发送指令';
|
String get repeater_cliSubtitle => '向复用器发送指令';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => '邻居';
|
String get repeater_neighbors => '邻居';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => '查看邻居节点(无需中间节点)。';
|
String get repeater_neighborsSubtitle => '查看邻居节点(无需中间节点)。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => '设置';
|
String get repeater_settings => '设置';
|
||||||
@@ -2251,7 +2251,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => '重复使用的邻居';
|
String get neighbors_repeatersNeighbors => '重复使用的邻居';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => '没有可用的邻居信息。';
|
String get neighbors_noData => '没有可用的邻居信息。';
|
||||||
@@ -2719,4 +2719,10 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject => 'meshcore-open GPX 地图数据导出';
|
String get settings_gpxExportShareSubject => 'meshcore-open GPX 地图数据导出';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => '附近的重复器';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => '最近访问';
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Buren",
|
"repeater_neighbors": "Buren",
|
||||||
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
|
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
|
||||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Herhalingen Buren",
|
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_runTrace": "Padeshulp traceren",
|
"map_runTrace": "Padeshulp traceren",
|
||||||
"scanner_enableBluetooth": "Activeer Bluetooth",
|
"scanner_enableBluetooth": "Activeer Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
|
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
|
||||||
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld"
|
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
|
||||||
|
"snrIndicator_lastSeen": "Laatst gezien",
|
||||||
|
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Sąsiedzi",
|
"repeater_neighbors": "Sąsiedzi",
|
||||||
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||||
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
||||||
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
||||||
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
|
"neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
|
||||||
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
|
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
|
||||||
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
|
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
|
||||||
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
|
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
|
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
|
||||||
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
|
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
|
||||||
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
||||||
"scanner_enableBluetooth": "Włącz Bluetooth"
|
"scanner_enableBluetooth": "Włącz Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Ostatnio widziany",
|
||||||
|
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vizinhos",
|
"repeater_neighbors": "Vizinhos",
|
||||||
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
|
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
|
||||||
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
|
"repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.",
|
||||||
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
|
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
|
||||||
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
|
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
|
"neighbors_repeatersNeighbors": "Repetidores Vizinhos",
|
||||||
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
|
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
|
||||||
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
||||||
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
|
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
|
||||||
"scanner_enableBluetooth": "Ative o Bluetooth",
|
"scanner_enableBluetooth": "Ative o Bluetooth",
|
||||||
"scanner_bluetoothOff": "Bluetooth está desativado",
|
"scanner_bluetoothOff": "Bluetooth está desativado",
|
||||||
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos."
|
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
||||||
|
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
|
||||||
|
"snrIndicator_lastSeen": "Visto pela última vez"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -472,8 +472,8 @@
|
|||||||
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
||||||
"repeater_cli": "CLI",
|
"repeater_cli": "CLI",
|
||||||
"repeater_cliSubtitle": "Отправка команд репитеру",
|
"repeater_cliSubtitle": "Отправка команд репитеру",
|
||||||
"repeater_neighbours": "Соседи",
|
"repeater_neighbors": "Соседи",
|
||||||
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
|
"repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.",
|
||||||
"repeater_settings": "Настройки",
|
"repeater_settings": "Настройки",
|
||||||
"repeater_settingsSubtitle": "Настройка параметров репитера",
|
"repeater_settingsSubtitle": "Настройка параметров репитера",
|
||||||
"repeater_statusTitle": "Статус репитера",
|
"repeater_statusTitle": "Статус репитера",
|
||||||
@@ -666,7 +666,7 @@
|
|||||||
"neighbors_receivedData": "Полученные данные о соседях",
|
"neighbors_receivedData": "Полученные данные о соседях",
|
||||||
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
||||||
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Соседи репитеров",
|
"neighbors_repeatersNeighbors": "Соседи репитеров",
|
||||||
"neighbors_noData": "Данные о соседях недоступны.",
|
"neighbors_noData": "Данные о соседях недоступны.",
|
||||||
"neighbors_unknownContact": "Неизвестный {pubkey}",
|
"neighbors_unknownContact": "Неизвестный {pubkey}",
|
||||||
"neighbors_heardA ago": "Слышали: {time} назад",
|
"neighbors_heardA ago": "Слышали: {time} назад",
|
||||||
@@ -838,5 +838,7 @@
|
|||||||
"map_runTrace": "Запустить трассировку пути",
|
"map_runTrace": "Запустить трассировку пути",
|
||||||
"scanner_enableBluetooth": "Включите Bluetooth",
|
"scanner_enableBluetooth": "Включите Bluetooth",
|
||||||
"scanner_bluetoothOff": "Bluetooth выключен",
|
"scanner_bluetoothOff": "Bluetooth выключен",
|
||||||
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства."
|
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
|
||||||
|
"snrIndicator_lastSeen": "Последний раз видели"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
|
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
|
||||||
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
||||||
"neighbors_receivedData": "Obdielo dáta suseda",
|
"neighbors_receivedData": "Obdielo dáta suseda",
|
||||||
"repeater_neighbours": "Súsezný",
|
"repeater_neighbors": "Súsezný",
|
||||||
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
|
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
|
"neighbors_repeatersNeighbors": "Opakovadlá Súsezná",
|
||||||
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
|
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
|
||||||
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
||||||
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
||||||
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
||||||
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
||||||
"scanner_enableBluetooth": "Povolte Bluetooth"
|
"scanner_enableBluetooth": "Povolte Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Naposledy videný",
|
||||||
|
"snrIndicator_nearByRepeaters": "Miestne opakovače"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
|
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
|
||||||
"repeater_neighbours": "Sosedi",
|
"repeater_neighbors": "Sosedi",
|
||||||
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
||||||
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
||||||
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
|
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
|
||||||
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
||||||
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
||||||
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
||||||
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
||||||
"scanner_bluetoothOff": "Bluetooth je izklopljen"
|
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
||||||
|
"snrIndicator_lastSeen": "Zadnjič videno",
|
||||||
|
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1356,12 +1356,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Grannar",
|
"repeater_neighbors": "Grannar",
|
||||||
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
|
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
|
||||||
"neighbors_receivedData": "Mottagna grannars data",
|
"neighbors_receivedData": "Mottagna grannars data",
|
||||||
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
||||||
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Upprepar grannar",
|
"neighbors_repeatersNeighbors": "Upprepar grannar",
|
||||||
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
||||||
"channels_createPrivateChannel": "Skapa en privat kanal",
|
"channels_createPrivateChannel": "Skapa en privat kanal",
|
||||||
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_removeLast": "Ta bort sista",
|
"map_removeLast": "Ta bort sista",
|
||||||
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
||||||
"scanner_bluetoothOff": "Bluetooth är avstängt"
|
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
||||||
|
"snrIndicator_lastSeen": "Senast sedd",
|
||||||
|
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1357,12 +1357,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Сусіди",
|
"repeater_neighbors": "Сусіди",
|
||||||
"repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.",
|
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
|
||||||
"neighbors_receivedData": "Дані сусідів отримано",
|
"neighbors_receivedData": "Дані сусідів отримано",
|
||||||
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
||||||
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ретранслятори-сусіди",
|
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
|
||||||
"neighbors_noData": "Дані про сусідів недоступні.",
|
"neighbors_noData": "Дані про сусідів недоступні.",
|
||||||
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
||||||
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||||
"scanner_bluetoothOff": "Bluetooth вимкнено"
|
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||||
|
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ближні ретранслятори"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -900,8 +900,8 @@
|
|||||||
"repeater_telemetrySubtitle": "查看传感器和系统状态的数据。",
|
"repeater_telemetrySubtitle": "查看传感器和系统状态的数据。",
|
||||||
"repeater_cli": "命令行界面",
|
"repeater_cli": "命令行界面",
|
||||||
"repeater_cliSubtitle": "向复用器发送指令",
|
"repeater_cliSubtitle": "向复用器发送指令",
|
||||||
"repeater_neighbours": "邻居",
|
"repeater_neighbors": "邻居",
|
||||||
"repeater_neighboursSubtitle": "查看邻居节点(无需中间节点)。",
|
"repeater_neighborsSubtitle": "查看邻居节点(无需中间节点)。",
|
||||||
"repeater_settings": "设置",
|
"repeater_settings": "设置",
|
||||||
"repeater_settingsSubtitle": "配置重复器参数",
|
"repeater_settingsSubtitle": "配置重复器参数",
|
||||||
"repeater_statusTitle": "重复器状态",
|
"repeater_statusTitle": "重复器状态",
|
||||||
@@ -1271,7 +1271,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"neighbors_repeatersNeighbours": "重复使用的邻居",
|
"neighbors_repeatersNeighbors": "重复使用的邻居",
|
||||||
"neighbors_noData": "没有可用的邻居信息。",
|
"neighbors_noData": "没有可用的邻居信息。",
|
||||||
"neighbors_unknownContact": "Unknown {pubkey}",
|
"neighbors_unknownContact": "Unknown {pubkey}",
|
||||||
"@neighbors_unknownContact": {
|
"@neighbors_unknownContact": {
|
||||||
@@ -1598,5 +1598,7 @@
|
|||||||
"map_runTrace": "运行路径跟踪",
|
"map_runTrace": "运行路径跟踪",
|
||||||
"scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。",
|
"scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。",
|
||||||
"scanner_bluetoothOff": "蓝牙已关闭",
|
"scanner_bluetoothOff": "蓝牙已关闭",
|
||||||
"scanner_enableBluetooth": "启用蓝牙"
|
"scanner_enableBluetooth": "启用蓝牙",
|
||||||
|
"snrIndicator_lastSeen": "最近访问",
|
||||||
|
"snrIndicator_nearByRepeaters": "附近的重复器"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class Contact {
|
|||||||
final pathBytes = _pathBytesForDisplay;
|
final pathBytes = _pathBytesForDisplay;
|
||||||
Uint8List? traceBytes;
|
Uint8List? traceBytes;
|
||||||
|
|
||||||
if (pathLength <= 0) {
|
if (pathBytes.isEmpty) {
|
||||||
traceBytes = Uint8List(1);
|
traceBytes = Uint8List(1);
|
||||||
traceBytes[0] = publicKey[0];
|
traceBytes[0] = publicKey[0];
|
||||||
return traceBytes;
|
return traceBytes;
|
||||||
@@ -160,9 +160,9 @@ class Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Contact? fromFrame(Uint8List data) {
|
static Contact? fromFrame(Uint8List data) {
|
||||||
if (data.length < contactFrameSize) return null;
|
if (data.isEmpty) return null;
|
||||||
if (data[0] != respCodeContact) return null;
|
if (data[0] != respCodeContact) return null;
|
||||||
|
try {
|
||||||
final pubKey = Uint8List.fromList(
|
final pubKey = Uint8List.fromList(
|
||||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||||
);
|
);
|
||||||
@@ -197,6 +197,10 @@ class Contact {
|
|||||||
longitude: lon,
|
longitude: lon,
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// If parsing fails, return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -901,7 +901,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChannelMessagePathScreen(message: message),
|
builder: (context) =>
|
||||||
|
ChannelMessagePathScreen(message: message, channelMessage: true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,27 @@ import '../models/contact.dart';
|
|||||||
|
|
||||||
class ChannelMessagePathScreen extends StatelessWidget {
|
class ChannelMessagePathScreen extends StatelessWidget {
|
||||||
final ChannelMessage message;
|
final ChannelMessage message;
|
||||||
|
final bool channelMessage;
|
||||||
const ChannelMessagePathScreen({super.key, required this.message});
|
const ChannelMessagePathScreen({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
this.channelMessage = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final primaryPath = _selectPrimaryPath(
|
final primaryPathTmp = _selectPrimaryPath(
|
||||||
message.pathBytes,
|
message.pathBytes,
|
||||||
message.pathVariants,
|
message.pathVariants,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final primaryPath = !channelMessage && !message.isOutgoing
|
||||||
|
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||||
|
: primaryPathTmp;
|
||||||
|
|
||||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
@@ -37,7 +46,6 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
l10n,
|
l10n,
|
||||||
);
|
);
|
||||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l10n.channelPath_title),
|
title: Text(l10n.channelPath_title),
|
||||||
@@ -50,9 +58,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: Uint8List.fromList(primaryPath),
|
path: primaryPath,
|
||||||
flipPathRound: true,
|
flipPathRound: true,
|
||||||
reversePathRound: true,
|
reversePathRound: !message.isOutgoing && !channelMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -62,7 +70,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
tooltip: l10n.channelPath_viewMap,
|
tooltip: l10n.channelPath_viewMap,
|
||||||
onPressed: hasHopDetails
|
onPressed: hasHopDetails
|
||||||
? () {
|
? () {
|
||||||
_openPathMap(context);
|
_openPathMap(context, channelMessage: channelMessage);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -157,7 +165,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
subtitle: Text(_formatPathPrefixes(variants[i])),
|
subtitle: Text(_formatPathPrefixes(variants[i])),
|
||||||
trailing: const Icon(Icons.map_outlined, size: 20),
|
trailing: const Icon(Icons.map_outlined, size: 20),
|
||||||
onTap: () => _openPathMap(context, initialPath: variants[i]),
|
onTap: () => _openPathMap(
|
||||||
|
context,
|
||||||
|
initialPath: variants[i],
|
||||||
|
channelMessage: channelMessage,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -248,13 +260,18 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
|
void _openPathMap(
|
||||||
|
BuildContext context, {
|
||||||
|
Uint8List? initialPath,
|
||||||
|
bool channelMessage = false,
|
||||||
|
}) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChannelMessagePathMapScreen(
|
builder: (context) => ChannelMessagePathMapScreen(
|
||||||
message: message,
|
message: message,
|
||||||
initialPath: initialPath,
|
initialPath: initialPath,
|
||||||
|
channelMessage: channelMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -264,11 +281,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
class ChannelMessagePathMapScreen extends StatefulWidget {
|
class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||||
final ChannelMessage message;
|
final ChannelMessage message;
|
||||||
final Uint8List? initialPath;
|
final Uint8List? initialPath;
|
||||||
|
final bool channelMessage;
|
||||||
|
|
||||||
const ChannelMessagePathMapScreen({
|
const ChannelMessagePathMapScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
this.initialPath,
|
this.initialPath,
|
||||||
|
this.channelMessage = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -323,11 +342,18 @@ class _ChannelMessagePathMapScreenState
|
|||||||
primaryPath,
|
primaryPath,
|
||||||
widget.message.pathVariants,
|
widget.message.pathVariants,
|
||||||
);
|
);
|
||||||
final selectedPath = _resolveSelectedPath(
|
final selectedPathTmp = _resolveSelectedPath(
|
||||||
_selectedPath,
|
_selectedPath,
|
||||||
observedPaths,
|
observedPaths,
|
||||||
primaryPath,
|
primaryPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final selectedPath =
|
||||||
|
((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(widget.message.isOutgoing && widget.channelMessage))
|
||||||
|
? Uint8List.fromList(selectedPathTmp.reversed.toList())
|
||||||
|
: selectedPathTmp;
|
||||||
|
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final hops = _buildPathHops(
|
final hops = _buildPathHops(
|
||||||
selectedPath,
|
selectedPath,
|
||||||
@@ -336,12 +362,24 @@ class _ChannelMessagePathMapScreenState
|
|||||||
);
|
);
|
||||||
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
|
print(
|
||||||
|
'outgoing: ${widget.message.isOutgoing}, channelMsg: ${widget.channelMessage}',
|
||||||
|
);
|
||||||
|
if ((widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(widget.message.isOutgoing && widget.channelMessage)) {
|
||||||
|
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
}
|
||||||
|
|
||||||
for (final hop in hops) {
|
for (final hop in hops) {
|
||||||
if (hop.hasLocation) {
|
if (hop.hasLocation) {
|
||||||
points.add(hop.position!);
|
points.add(hop.position!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(!widget.message.isOutgoing && widget.channelMessage)) {
|
||||||
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
}
|
||||||
|
|
||||||
final polylines = points.length > 1
|
final polylines = points.length > 1
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:math';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -14,7 +15,6 @@ import '../storage/community_store.dart';
|
|||||||
import '../utils/dialog_utils.dart';
|
import '../utils/dialog_utils.dart';
|
||||||
import '../utils/disconnect_navigation_mixin.dart';
|
import '../utils/disconnect_navigation_mixin.dart';
|
||||||
import '../utils/route_transitions.dart';
|
import '../utils/route_transitions.dart';
|
||||||
import '../widgets/battery_indicator.dart';
|
|
||||||
import '../widgets/list_filter_widget.dart';
|
import '../widgets/list_filter_widget.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
import '../widgets/qr_code_display.dart';
|
import '../widgets/qr_code_display.dart';
|
||||||
@@ -116,8 +116,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
canPop: allowBack,
|
canPop: allowBack,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BatteryIndicator(connector: connector),
|
title: AppBarTitle(context.l10n.channels_title),
|
||||||
title: Text(context.l10n.channels_title),
|
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -437,6 +437,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
builder: (context) => Consumer<PathHistoryService>(
|
builder: (context) => Consumer<PathHistoryService>(
|
||||||
builder: (context, pathService, _) {
|
builder: (context, pathService, _) {
|
||||||
final paths = pathService.getRecentPaths(widget.contact.publicKeyHex);
|
final paths = pathService.getRecentPaths(widget.contact.publicKeyHex);
|
||||||
|
|
||||||
|
final repeatersList = List.of(connector.directRepeaters)
|
||||||
|
..sort((a, b) => b.ranking.compareTo(a.ranking));
|
||||||
|
|
||||||
|
final directRepeater = repeatersList.isEmpty
|
||||||
|
? null
|
||||||
|
: repeatersList.first;
|
||||||
|
final secondDirectRepeater = repeatersList.length < 2
|
||||||
|
? null
|
||||||
|
: repeatersList.elementAt(1);
|
||||||
|
final thirdDirectRepeater = repeatersList.length < 3
|
||||||
|
? null
|
||||||
|
: repeatersList.elementAt(2);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -478,15 +492,38 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
],
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...paths.map((path) {
|
...paths.map((path) {
|
||||||
|
final isDirectRepeater =
|
||||||
|
directRepeater != null &&
|
||||||
|
path.pathBytes.isNotEmpty &&
|
||||||
|
directRepeater.pubkeyFirstByte ==
|
||||||
|
path.pathBytes.first;
|
||||||
|
final isSecondDirectRepeater =
|
||||||
|
secondDirectRepeater != null &&
|
||||||
|
path.pathBytes.isNotEmpty &&
|
||||||
|
secondDirectRepeater.pubkeyFirstByte ==
|
||||||
|
path.pathBytes.first;
|
||||||
|
final isThirdDirectRepeater =
|
||||||
|
thirdDirectRepeater != null &&
|
||||||
|
path.pathBytes.isNotEmpty &&
|
||||||
|
thirdDirectRepeater.pubkeyFirstByte ==
|
||||||
|
path.pathBytes.first;
|
||||||
|
Color color = Colors.grey;
|
||||||
|
if (isDirectRepeater) {
|
||||||
|
color = Colors.green;
|
||||||
|
} else if (isSecondDirectRepeater) {
|
||||||
|
color = Colors.yellow;
|
||||||
|
} else if (isThirdDirectRepeater) {
|
||||||
|
color = Colors.red;
|
||||||
|
} else if (path.wasFloodDiscovery) {
|
||||||
|
color = Colors.blue;
|
||||||
|
}
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: path.wasFloodDiscovery
|
backgroundColor: color,
|
||||||
? Colors.blue
|
|
||||||
: Colors.green,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'${path.hopCount}',
|
'${path.hopCount}',
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
|
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -16,7 +18,6 @@ import '../utils/dialog_utils.dart';
|
|||||||
import '../utils/disconnect_navigation_mixin.dart';
|
import '../utils/disconnect_navigation_mixin.dart';
|
||||||
import '../utils/emoji_utils.dart';
|
import '../utils/emoji_utils.dart';
|
||||||
import '../utils/route_transitions.dart';
|
import '../utils/route_transitions.dart';
|
||||||
import '../widgets/battery_indicator.dart';
|
|
||||||
import '../widgets/list_filter_widget.dart';
|
import '../widgets/list_filter_widget.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
import '../widgets/quick_switch_bar.dart';
|
import '../widgets/quick_switch_bar.dart';
|
||||||
@@ -90,6 +91,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
if (frame.isEmpty) return;
|
if (frame.isEmpty) return;
|
||||||
final frameBuffer = BufferReader(frame);
|
final frameBuffer = BufferReader(frame);
|
||||||
|
try {
|
||||||
final code = frameBuffer.readUInt8();
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
if (code == respCodeExportContact) {
|
if (code == respCodeExportContact) {
|
||||||
@@ -130,7 +132,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
|
|
||||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +147,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
|
|
||||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_contactImportFailed),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +170,12 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
|
|
||||||
_pendingOperations.clear();
|
_pendingOperations.clear();
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error(
|
||||||
|
'Error processing received frame: $e',
|
||||||
|
tag: 'ContactsScreen',
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,9 +241,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
canPop: allowBack,
|
canPop: allowBack,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BatteryIndicator(connector: connector),
|
title: AppBarTitle(context.l10n.contacts_title),
|
||||||
title: Text(context.l10n.contacts_title),
|
|
||||||
centerTitle: true,
|
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
|
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -17,7 +18,6 @@ import '../services/map_marker_service.dart';
|
|||||||
import '../services/map_tile_cache_service.dart';
|
import '../services/map_tile_cache_service.dart';
|
||||||
import '../utils/contact_search.dart';
|
import '../utils/contact_search.dart';
|
||||||
import '../utils/route_transitions.dart';
|
import '../utils/route_transitions.dart';
|
||||||
import '../widgets/battery_indicator.dart';
|
|
||||||
import '../widgets/quick_switch_bar.dart';
|
import '../widgets/quick_switch_bar.dart';
|
||||||
import 'channels_screen.dart';
|
import 'channels_screen.dart';
|
||||||
import 'chat_screen.dart';
|
import 'chat_screen.dart';
|
||||||
@@ -105,7 +105,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
|
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
|
||||||
final maxSpread = max(latStdDev, lonStdDev);
|
final maxSpread = max(latStdDev, lonStdDev);
|
||||||
if (maxSpread <= 0) return 13.0;
|
if (maxSpread <= 0) return 13.0;
|
||||||
// Approzimate: each zoom level halves the visible area
|
// Approximate: each zoom level halves the visible area
|
||||||
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
|
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
|
||||||
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
|
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
|
||||||
return zoom.clamp(4.0, 15.0);
|
return zoom.clamp(4.0, 15.0);
|
||||||
@@ -262,8 +262,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
canPop: allowBack,
|
canPop: allowBack,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BatteryIndicator(connector: connector),
|
title: AppBarTitle(context.l10n.map_title),
|
||||||
title: Text(context.l10n.map_title),
|
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
@@ -384,8 +383,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
connector.selfLatitude!,
|
connector.selfLatitude!,
|
||||||
connector.selfLongitude!,
|
connector.selfLongitude!,
|
||||||
),
|
),
|
||||||
width: 35,
|
width: 40,
|
||||||
height: 35,
|
height: 40,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -826,7 +825,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
color: _getNodeColor(contact.type),
|
color: _getNodeColor(contact.type),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: Text(contact.name)),
|
Expanded(child: SelectableText(contact.name)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: Column(
|
content: Column(
|
||||||
@@ -997,7 +996,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(value, style: const TextStyle(fontSize: 14)),
|
SelectableText(value, style: const TextStyle(fontSize: 14)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
@@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart';
|
|||||||
import '../widgets/path_management_dialog.dart';
|
import '../widgets/path_management_dialog.dart';
|
||||||
import '../widgets/snr_indicator.dart';
|
import '../widgets/snr_indicator.dart';
|
||||||
|
|
||||||
class NeighboursScreen extends StatefulWidget {
|
class NeighborsScreen extends StatefulWidget {
|
||||||
final Contact repeater;
|
final Contact repeater;
|
||||||
final String password;
|
final String password;
|
||||||
|
|
||||||
const NeighboursScreen({
|
const NeighborsScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.repeater,
|
required this.repeater,
|
||||||
required this.password,
|
required this.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NeighboursScreen> createState() => _NeighboursScreenState();
|
State<NeighborsScreen> createState() => _NeighborsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NeighboursScreenState extends State<NeighboursScreen> {
|
class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||||
static const int _reqNeighboursKeyLen = 4;
|
static const int _reqNeighborsKeyLen = 4;
|
||||||
static const int _statusPayloadOffset = 8;
|
static const int _statusPayloadOffset = 8;
|
||||||
static const int _statusStatsSize = 52;
|
static const int _statusStatsSize = 52;
|
||||||
static const int _statusResponseBytes =
|
static const int _statusResponseBytes =
|
||||||
_statusPayloadOffset + _statusStatsSize;
|
_statusPayloadOffset + _statusStatsSize;
|
||||||
Uint8List _tagData = Uint8List(4);
|
Uint8List _tagData = Uint8List(4);
|
||||||
int _neighbourCount = 0;
|
int _neighborCount = 0;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
@@ -41,7 +42,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
RepeaterCommandService? _commandService;
|
RepeaterCommandService? _commandService;
|
||||||
PathSelection? _pendingStatusSelection;
|
PathSelection? _pendingStatusSelection;
|
||||||
List<Map<String, dynamic>>? _parsedNeighbours;
|
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -49,7 +50,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
_commandService = RepeaterCommandService(connector);
|
_commandService = RepeaterCommandService(connector);
|
||||||
_setupMessageListener();
|
_setupMessageListener();
|
||||||
_loadNeighbours();
|
_loadNeighbors();
|
||||||
_hasData = false;
|
_hasData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,13 +63,12 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
|
|
||||||
if (frame[0] == respCodeSent) {
|
if (frame[0] == respCodeSent) {
|
||||||
_tagData = frame.sublist(2, 6);
|
_tagData = frame.sublist(2, 6);
|
||||||
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a binary response
|
// Check if it's a binary response
|
||||||
if (frame[0] == pushCodeBinaryResponse &&
|
if (frame[0] == pushCodeBinaryResponse &&
|
||||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||||
_handleNeighboursResponse(connector, frame.sublist(6));
|
_handleNeighborsResponse(connector, frame.sublist(6));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,13 +91,14 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
return '${h}h ${m2}m';
|
return '${h}h ${m2}m';
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Map<String, dynamic>> parseNeighboursData(
|
static List<Map<String, dynamic>> parseNeighborsData(
|
||||||
BufferReader buffer,
|
BufferReader buffer,
|
||||||
int resultsCount,
|
int resultsCount,
|
||||||
) {
|
) {
|
||||||
final Map<int, Map<String, dynamic>> neighbours = {};
|
final Map<int, Map<String, dynamic>> neighbors = {};
|
||||||
|
try {
|
||||||
for (var i = 0; i < resultsCount; i++) {
|
for (var i = 0; i < resultsCount; i++) {
|
||||||
final neighbourData = neighbours.putIfAbsent(
|
final neighborData = neighbors.putIfAbsent(
|
||||||
i,
|
i,
|
||||||
() => {
|
() => {
|
||||||
'contact': null,
|
'contact': null,
|
||||||
@@ -106,35 +107,43 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
'snr': <double>{},
|
'snr': <double>{},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
|
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
|
||||||
neighbourData['lastHeard'] = buffer.readUInt32LE();
|
neighborData['lastHeard'] = buffer.readUInt32LE();
|
||||||
neighbourData['snr'] = buffer.readInt8() / 4.0;
|
neighborData['snr'] = buffer.readInt8() / 4.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return neighbours.values.toList();
|
return neighbors.values.toList();
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error(
|
||||||
|
'Error parsing neighbors data: $e',
|
||||||
|
tag: 'NeighborsScreen',
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
|
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||||
final buffer = BufferReader(frame);
|
final buffer = BufferReader(frame);
|
||||||
final neighbourCount = buffer.readUInt16LE();
|
try {
|
||||||
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
|
final neighborCount = buffer.readUInt16LE();
|
||||||
|
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
||||||
repeater,
|
repeater,
|
||||||
) {
|
) {
|
||||||
for (var neighbourData in parsedNeighbours) {
|
for (var neighborData in parsedNeighbors) {
|
||||||
final publicKey = neighbourData['publicKey'];
|
final publicKey = neighborData['publicKey'];
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
|
repeater.publicKey.sublist(0, _reqNeighborsKeyLen),
|
||||||
publicKey,
|
publicKey,
|
||||||
)) {
|
)) {
|
||||||
neighbourData['contact'] = repeater;
|
neighborData['contact'] = repeater;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_parsedNeighbours = parsedNeighbours;
|
_parsedNeighbors = parsedNeighbors;
|
||||||
_neighbourCount = neighbourCount;
|
_neighborCount = neighborCount;
|
||||||
});
|
});
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -150,6 +159,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
_hasData = true;
|
_hasData = true;
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error('Error handling neighbors response: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||||
@@ -159,7 +171,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadNeighbours() async {
|
Future<void> _loadNeighbors() async {
|
||||||
if (_commandService == null) return;
|
if (_commandService == null) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -172,17 +184,17 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
final selection = await connector.preparePathForContactSend(repeater);
|
final selection = await connector.preparePathForContactSend(repeater);
|
||||||
_pendingStatusSelection = selection;
|
_pendingStatusSelection = selection;
|
||||||
|
|
||||||
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
|
//[version][number of requested neighbors][offset_16bit][order by][len of public key]
|
||||||
final frame = buildSendBinaryReq(
|
final frame = buildSendBinaryReq(
|
||||||
repeater.publicKey,
|
repeater.publicKey,
|
||||||
payload: Uint8List.fromList([
|
payload: Uint8List.fromList([
|
||||||
reqTypeGetNeighbours,
|
reqTypeGetNeighbors,
|
||||||
0x00,
|
0x00,
|
||||||
0x0F,
|
0x0F,
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
_reqNeighboursKeyLen,
|
_reqNeighborsKeyLen,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
await connector.sendFrame(frame);
|
await connector.sendFrame(frame);
|
||||||
@@ -258,7 +270,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
l10n.neighbors_repeatersNeighbours,
|
l10n.neighbors_repeatersNeighbors,
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -345,7 +357,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.refresh),
|
: const Icon(Icons.refresh),
|
||||||
onPressed: _isLoading ? null : _loadNeighbours,
|
onPressed: _isLoading ? null : _loadNeighbors,
|
||||||
tooltip: l10n.repeater_refresh,
|
tooltip: l10n.repeater_refresh,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -353,13 +365,13 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: _loadNeighbours,
|
onRefresh: _loadNeighbors,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
if (!_isLoaded &&
|
if (!_isLoaded &&
|
||||||
!_hasData &&
|
!_hasData &&
|
||||||
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
|
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.neighbors_noData,
|
l10n.neighbors_noData,
|
||||||
@@ -368,10 +380,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
),
|
),
|
||||||
if (_isLoaded ||
|
if (_isLoaded ||
|
||||||
_hasData &&
|
_hasData &&
|
||||||
!(_parsedNeighbours == null ||
|
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||||
_parsedNeighbours!.isEmpty))
|
_buildNeighborsInfoCard(
|
||||||
_buildNeighboursInfoCard(
|
"${l10n.repeater_neighbors} - $_neighborCount",
|
||||||
"${l10n.repeater_neighbours} - $_neighbourCount",
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -380,7 +391,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNeighboursInfoCard(String title) {
|
Widget _buildNeighborsInfoCard(String title) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -405,7 +416,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
for (final entry in _parsedNeighbours!.asMap().entries)
|
for (final entry in _parsedNeighbors!.asMap().entries)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
entry.value['contact'] != null
|
entry.value['contact'] != null
|
||||||
? entry.value['contact'].name
|
? entry.value['contact'].name
|
||||||
@@ -430,6 +441,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
double snr,
|
double snr,
|
||||||
int spreadingFactor,
|
int spreadingFactor,
|
||||||
) {
|
) {
|
||||||
|
final snrUi = snrUiFromSNR(snr, spreadingFactor);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -443,9 +455,15 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
subtitle: Text(value),
|
subtitle: Text(value),
|
||||||
trailing: SNRIcon(
|
trailing: Column(
|
||||||
snr: snr,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
snrLevels: getSNRfromSF(spreadingFactor),
|
children: [
|
||||||
|
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
|
||||||
|
Text(
|
||||||
|
snrUi.text,
|
||||||
|
style: TextStyle(fontSize: 10, color: snrUi.color),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -10,6 +10,7 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
|||||||
import 'package:meshcore_open/l10n/l10n.dart';
|
import 'package:meshcore_open/l10n/l10n.dart';
|
||||||
import 'package:meshcore_open/models/contact.dart';
|
import 'package:meshcore_open/models/contact.dart';
|
||||||
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ String formatDistance(double distanceMeters) {
|
|||||||
|
|
||||||
class PathTraceData {
|
class PathTraceData {
|
||||||
final Uint8List pathData;
|
final Uint8List pathData;
|
||||||
final Uint8List snrData;
|
final List<double> snrData;
|
||||||
final Map<int, Contact> pathContacts;
|
final Map<int, Contact> pathContacts;
|
||||||
|
|
||||||
PathTraceData({
|
PathTraceData({
|
||||||
@@ -45,6 +46,7 @@ class PathTraceData {
|
|||||||
class PathTraceMapScreen extends StatefulWidget {
|
class PathTraceMapScreen extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Uint8List path;
|
final Uint8List path;
|
||||||
|
final int? repeaterId;
|
||||||
final bool flipPathRound;
|
final bool flipPathRound;
|
||||||
final bool reversePathRound;
|
final bool reversePathRound;
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.path,
|
required this.path,
|
||||||
|
this.repeaterId,
|
||||||
this.flipPathRound = false,
|
this.flipPathRound = false,
|
||||||
this.reversePathRound = false,
|
this.reversePathRound = false,
|
||||||
});
|
});
|
||||||
@@ -96,7 +99,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List addReturnpath(Uint8List pathBytes) {
|
Uint8List addReturnPath(Uint8List pathBytes) {
|
||||||
Uint8List? traceBytes;
|
Uint8List? traceBytes;
|
||||||
final len = (pathBytes.length + pathBytes.length - 1);
|
final len = (pathBytes.length + pathBytes.length - 1);
|
||||||
traceBytes = Uint8List(len);
|
traceBytes = Uint8List(len);
|
||||||
@@ -124,11 +127,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
: widget.path;
|
: widget.path;
|
||||||
|
|
||||||
if (widget.flipPathRound) {
|
if (widget.flipPathRound) {
|
||||||
path = addReturnpath(pathTmp);
|
path = addReturnPath(pathTmp);
|
||||||
} else {
|
} else {
|
||||||
path = pathTmp;
|
path = pathTmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('Initiating path trace with path: ${_formatPathPrefixes(path)}');
|
||||||
|
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
final frame = buildTraceReq(
|
final frame = buildTraceReq(
|
||||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
@@ -146,6 +151,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
if (frame.isEmpty) return;
|
if (frame.isEmpty) return;
|
||||||
final frameBuffer = BufferReader(frame);
|
final frameBuffer = BufferReader(frame);
|
||||||
|
try {
|
||||||
final code = frameBuffer.readUInt8();
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
if (code == respCodeSent) {
|
if (code == respCodeSent) {
|
||||||
@@ -172,6 +178,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
_failed2Loaded = true;
|
_failed2Loaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a binary response
|
// Check if it's a binary response
|
||||||
if (frame.length > 8 &&
|
if (frame.length > 8 &&
|
||||||
code == pushCodeTraceData &&
|
code == pushCodeTraceData &&
|
||||||
@@ -183,6 +190,16 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
_handleTraceResponse(frame);
|
_handleTraceResponse(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
// Handle any parsing errors gracefully
|
||||||
|
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +207,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
final buffer = BufferReader(frame);
|
final buffer = BufferReader(frame);
|
||||||
|
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
|
buffer.skipBytes(5); // Skip Flag byte and tag data
|
||||||
@@ -199,7 +217,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
|
|
||||||
Map<int, Contact> pathContacts = {};
|
Map<int, Contact> pathContacts = {};
|
||||||
|
|
||||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
||||||
|
repeater,
|
||||||
|
) {
|
||||||
for (var repeaterData in pathData) {
|
for (var repeaterData in pathData) {
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
repeater.publicKey.sublist(0, 1),
|
repeater.publicKey.sublist(0, 1),
|
||||||
@@ -215,7 +235,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
_hasData = true;
|
_hasData = true;
|
||||||
_traceData = PathTraceData(
|
_traceData = PathTraceData(
|
||||||
pathData: pathData,
|
pathData: pathData,
|
||||||
snrData: snrData,
|
snrData: snrData.map((e) => e.toSigned(8).toDouble() / 4).toList(),
|
||||||
pathContacts: pathContacts,
|
pathContacts: pathContacts,
|
||||||
);
|
);
|
||||||
_points = <LatLng>[];
|
_points = <LatLng>[];
|
||||||
@@ -239,7 +259,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
]
|
]
|
||||||
: <Polyline>[];
|
: <Polyline>[];
|
||||||
|
|
||||||
_initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0);
|
_initialCenter = _points.isNotEmpty
|
||||||
|
? _points.first
|
||||||
|
: const LatLng(0, 0);
|
||||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||||
_mapKey = ValueKey(
|
_mapKey = ValueKey(
|
||||||
@@ -247,6 +269,18 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
);
|
);
|
||||||
_pathDistanceMeters = getPathDistanceMeters(_points);
|
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error(
|
||||||
|
'Error handling trace response: $e',
|
||||||
|
tag: 'PathTraceMapScreen',
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -532,6 +566,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
itemCount: pathTraceData.pathData.length + 1,
|
itemCount: pathTraceData.pathData.length + 1,
|
||||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
final snrUi = snrUiFromSNR(
|
||||||
|
index < pathTraceData.snrData.length
|
||||||
|
? pathTraceData.snrData[index]
|
||||||
|
: null,
|
||||||
|
context.read<MeshCoreConnector>().currentSf,
|
||||||
|
);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -550,12 +590,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
trailing: SNRIcon(
|
trailing: Column(
|
||||||
snr:
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
pathTraceData.snrData[index].toSigned(
|
children: [
|
||||||
8,
|
Icon(
|
||||||
) /
|
snrUi.icon,
|
||||||
4.0,
|
color: snrUi.color,
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
snrUi.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: snrUi.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Handle item tap
|
// Handle item tap
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'repeater_status_screen.dart';
|
|||||||
import 'repeater_cli_screen.dart';
|
import 'repeater_cli_screen.dart';
|
||||||
import 'repeater_settings_screen.dart';
|
import 'repeater_settings_screen.dart';
|
||||||
import 'telemetry_screen.dart';
|
import 'telemetry_screen.dart';
|
||||||
import 'neighbours_screen.dart';
|
import 'neighbors_screen.dart';
|
||||||
|
|
||||||
class RepeaterHubScreen extends StatelessWidget {
|
class RepeaterHubScreen extends StatelessWidget {
|
||||||
final Contact repeater;
|
final Contact repeater;
|
||||||
@@ -174,17 +174,15 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||||||
_buildManagementCard(
|
_buildManagementCard(
|
||||||
context,
|
context,
|
||||||
icon: Icons.group,
|
icon: Icons.group,
|
||||||
title: l10n.repeater_neighbours,
|
title: l10n.repeater_neighbors,
|
||||||
subtitle: l10n.repeater_neighboursSubtitle,
|
subtitle: l10n.repeater_neighborsSubtitle,
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => NeighboursScreen(
|
builder: (context) =>
|
||||||
repeater: repeater,
|
NeighborsScreen(repeater: repeater, password: password),
|
||||||
password: password,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
|
import 'package:meshcore_open/widgets/battery_indicator.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'snr_indicator.dart';
|
||||||
|
|
||||||
|
class AppBarTitle extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final Widget? leading;
|
||||||
|
final Widget? trailing;
|
||||||
|
const AppBarTitle(this.title, {this.leading, this.trailing, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
leading ?? const SizedBox.shrink(),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
leading ?? const SizedBox.shrink(),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (connector.isConnected && connector.selfName != null)
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'(${connector.selfName})',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
BatteryIndicator(connector: connector),
|
||||||
|
SNRIndicator(connector: connector),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing ?? const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,10 +66,13 @@ class _BatteryIndicatorState extends State<BatteryIndicator> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(batteryUi.icon, size: 18, color: batteryUi.color),
|
Icon(batteryUi.icon, size: 18, color: batteryUi.color),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(height: 2),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
displayText,
|
displayText,
|
||||||
@@ -78,13 +81,14 @@ class _BatteryIndicatorState extends State<BatteryIndicator> {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: batteryUi.color,
|
color: batteryUi.color,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,19 @@ class _PathManagementDialog extends StatelessWidget {
|
|||||||
final currentContact = _resolveContact(connector);
|
final currentContact = _resolveContact(connector);
|
||||||
final paths = pathService.getRecentPaths(currentContact.publicKeyHex);
|
final paths = pathService.getRecentPaths(currentContact.publicKeyHex);
|
||||||
|
|
||||||
|
final repeatersList = List.of(connector.directRepeaters)
|
||||||
|
..sort((a, b) => b.ranking.compareTo(a.ranking));
|
||||||
|
|
||||||
|
final directRepeater = repeatersList.isEmpty
|
||||||
|
? null
|
||||||
|
: repeatersList.first;
|
||||||
|
final secondDirectRepeater = repeatersList.length < 2
|
||||||
|
? null
|
||||||
|
: repeatersList.elementAt(1);
|
||||||
|
final thirdDirectRepeater = repeatersList.length < 3
|
||||||
|
? null
|
||||||
|
: repeatersList.elementAt(2);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(l10n.chat_pathManagement),
|
title: Text(l10n.chat_pathManagement),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
@@ -174,15 +187,38 @@ class _PathManagementDialog extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...paths.map((path) {
|
...paths.map((path) {
|
||||||
|
final isDirectRepeater =
|
||||||
|
directRepeater != null &&
|
||||||
|
path.pathBytes.isNotEmpty &&
|
||||||
|
directRepeater.pubkeyFirstByte == path.pathBytes.first;
|
||||||
|
final isSecondDirectRepeater =
|
||||||
|
secondDirectRepeater != null &&
|
||||||
|
path.pathBytes.isNotEmpty &&
|
||||||
|
secondDirectRepeater.pubkeyFirstByte ==
|
||||||
|
path.pathBytes.first;
|
||||||
|
final isThirdDirectRepeater =
|
||||||
|
thirdDirectRepeater != null &&
|
||||||
|
path.pathBytes.isNotEmpty &&
|
||||||
|
thirdDirectRepeater.pubkeyFirstByte ==
|
||||||
|
path.pathBytes.first;
|
||||||
|
|
||||||
|
Color color = Colors.grey;
|
||||||
|
if (isDirectRepeater) {
|
||||||
|
color = Colors.green;
|
||||||
|
} else if (isSecondDirectRepeater) {
|
||||||
|
color = Colors.yellow;
|
||||||
|
} else if (isThirdDirectRepeater) {
|
||||||
|
color = Colors.red;
|
||||||
|
} else if (path.wasFloodDiscovery) {
|
||||||
|
color = Colors.blue;
|
||||||
|
}
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: path.wasFloodDiscovery
|
backgroundColor: color,
|
||||||
? Colors.blue
|
|
||||||
: Colors.green,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'${path.hopCount}',
|
'${path.hopCount}',
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
|
|||||||
+157
-14
@@ -1,4 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
|
class SNRUi {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final String text;
|
||||||
|
const SNRUi(this.icon, this.color, this.text);
|
||||||
|
}
|
||||||
|
|
||||||
List<double> getSNRfromSF(int spreadingFactor) {
|
List<double> getSNRfromSF(int spreadingFactor) {
|
||||||
switch (spreadingFactor) {
|
switch (spreadingFactor) {
|
||||||
@@ -19,20 +28,19 @@ List<double> getSNRfromSF(int spreadingFactor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SNRIcon extends StatelessWidget {
|
SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) {
|
||||||
final double snr;
|
if (snr == null ||
|
||||||
final List<double> snrLevels;
|
spreadingFactor == null ||
|
||||||
|
spreadingFactor < 7 ||
|
||||||
|
spreadingFactor > 12) {
|
||||||
|
return const SNRUi(Icons.signal_cellular_off, Colors.grey, '—');
|
||||||
|
}
|
||||||
|
|
||||||
const SNRIcon({
|
final snrLevels = getSNRfromSF(spreadingFactor);
|
||||||
super.key,
|
|
||||||
required this.snr,
|
|
||||||
this.snrLevels = const [4.0, -2.0, -4.0, -6.0],
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
IconData icon;
|
IconData icon;
|
||||||
Color color;
|
Color color;
|
||||||
|
String text = '${snr.toStringAsFixed(1)} dB';
|
||||||
|
|
||||||
if (snr >= snrLevels[0]) {
|
if (snr >= snrLevels[0]) {
|
||||||
icon = Icons.signal_cellular_alt;
|
icon = Icons.signal_cellular_alt;
|
||||||
@@ -51,12 +59,147 @@ class SNRIcon extends StatelessWidget {
|
|||||||
color = Colors.red;
|
color = Colors.red;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return SNRUi(icon, color, text);
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
}
|
||||||
|
|
||||||
|
class SNRIndicator extends StatefulWidget {
|
||||||
|
final MeshCoreConnector connector;
|
||||||
|
|
||||||
|
const SNRIndicator({super.key, required this.connector});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SNRIndicator> createState() => _SNRIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SNRIndicatorState extends State<SNRIndicator> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final directRepeaters = widget.connector.directRepeaters;
|
||||||
|
final directBestRepeaters = List.of(directRepeaters)
|
||||||
|
..sort((a, b) => (b.ranking).compareTo(a.ranking));
|
||||||
|
final directRepeater = directBestRepeaters.isEmpty
|
||||||
|
? null
|
||||||
|
: directBestRepeaters.first;
|
||||||
|
|
||||||
|
final snrUi = snrUiFromSNR(
|
||||||
|
directBestRepeaters.isNotEmpty ? directRepeater!.snr : null,
|
||||||
|
widget.connector.currentSf,
|
||||||
|
);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (directRepeater != null) {
|
||||||
|
_showFullPathDialog(context, directBestRepeaters);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: color),
|
Column(
|
||||||
Text('$snr dB', style: TextStyle(fontSize: 10, color: color)),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(snrUi.icon, size: 18, color: snrUi.color),
|
||||||
|
Text(
|
||||||
|
snrUi.text,
|
||||||
|
style: TextStyle(fontSize: 12, color: snrUi.color),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
if (directRepeater != null)
|
||||||
|
Text(
|
||||||
|
'${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatLastUpdated(DateTime lastSeen) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.difference(lastSeen);
|
||||||
|
if (diff.isNegative) {
|
||||||
|
return "0s";
|
||||||
|
}
|
||||||
|
if (diff.inMinutes < 1) {
|
||||||
|
return "${diff.inSeconds}s";
|
||||||
|
}
|
||||||
|
if (diff.inMinutes < 60) {
|
||||||
|
return "${diff.inMinutes}m";
|
||||||
|
}
|
||||||
|
if (diff.inHours < 24) {
|
||||||
|
final hours = diff.inHours;
|
||||||
|
return hours == 1 ? "1h" : "${hours}hs";
|
||||||
|
}
|
||||||
|
final days = diff.inDays;
|
||||||
|
return days == 1 ? "1d" : "${days}ds";
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFullPathDialog(
|
||||||
|
BuildContext context,
|
||||||
|
List<DirectRepeater> directBestRepeaters,
|
||||||
|
) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(l10n.snrIndicator_nearByRepeaters),
|
||||||
|
content: SizedBox(
|
||||||
|
child: Scrollbar(
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
itemCount: directBestRepeaters.length,
|
||||||
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final repeater = directBestRepeaters[index];
|
||||||
|
final snrUi = snrUiFromSNR(
|
||||||
|
repeater.snr,
|
||||||
|
widget.connector.currentSf,
|
||||||
|
);
|
||||||
|
|
||||||
|
final name = widget.connector.contacts
|
||||||
|
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
|
||||||
|
.map((c) => c.name)
|
||||||
|
.firstOrNull;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(snrUi.icon, color: snrUi.color),
|
||||||
|
title: Text(
|
||||||
|
name ??
|
||||||
|
repeater.pubkeyFirstByte
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0'),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.common_close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -69,10 +69,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -497,18 +497,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.18"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -910,10 +910,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.9"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user