Compare commits

...

28 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 3a06c36ec4 Wrap title Column in Expanded to prevent horizontal overflow in AppBarTitle
Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com>
2026-02-18 23:51:49 +00:00
copilot-swe-agent[bot] 302589f9f4 Initial plan 2026-02-18 23:49:50 +00:00
Winston Lowe b8acedd03e Refactor path handling in ChannelMessagePathScreen to improve logic for outgoing messages and channel messages 2026-02-18 10:25:06 -08:00
Winston Lowe 17a9db0f0e Refactor ranking calculation for direct repeaters and update path handling in channel message screens 2026-02-18 09:28:25 -08:00
Winston Lowe 0a01ecde38 Fix typo in variable name for second direct repeater in path management dialog 2026-02-16 15:07:31 -08:00
Winston Lowe 14cec533ac Adjust ranking calculation for direct repeaters by adding offset to SNR for improved accuracy 2026-02-16 12:58:55 -08:00
Winston Lowe fdfc1f6d25 Refactor localization keys for "neighbors" terminology across multiple languages
- Updated localization keys from "neighbours" to "neighbors" in the following files:
  - app_localizations_bg.dart
  - app_localizations_de.dart
  - app_localizations_en.dart
  - app_localizations_es.dart
  - app_localizations_fr.dart
  - app_localizations_it.dart
  - app_localizations_nl.dart
  - app_localizations_pl.dart
  - app_localizations_pt.dart
  - app_localizations_ru.dart
  - app_localizations_sk.dart
  - app_localizations_sl.dart
  - app_localizations_sv.dart
  - app_localizations_uk.dart
  - app_localizations_zh.dart
- Updated corresponding ARB files to reflect the changes in keys.
- Renamed the NeighboursScreen to NeighborsScreen in the chat and repeater hub screens for consistency.
2026-02-16 12:58:37 -08:00
Winston Lowe 42eb293d1c Implement ranking system for direct repeaters based on SNR and recency; update related UI components to reflect changes 2026-02-16 11:58:44 -08:00
Winston Lowe 36401210ce Prevent notifications for chat and sensor adverts without a valid path 2026-02-15 21:16:42 -08:00
Winston Lowe a68e1dd428 Sort direct repeaters by last updated time and SNR; limit to top three for improved path management dialog 2026-02-15 21:09:30 -08:00
Winston Lowe 52a578777d Refactor AppBarTitle widget to remove unused style parameter; update related screens to reflect changes
Improve SNR handling by adding validation for spreading factor range in snrUiFromSNR function
Update contact handling in MeshCoreConnector to fix variable naming and improve readability
Stop parsing unsupported LPP types in CayenneLpp to avoid misalignment
2026-02-15 19:38:34 -08:00
Winston Lowe 71152bd3eb Throw an exception for unsupported LPP types in CayenneLpp class 2026-02-15 17:20:35 -08:00
Winston Lowe 940a1be203 Handle loading state and error parsing in PathTraceMapScreen; update SNR indicator dialog content layout 2026-02-15 17:16:56 -08:00
Winston Lowe 75a7f437f6 Add SNR indicator localization and update UI references for nearby repeaters 2026-02-15 17:04:54 -08:00
Winston Lowe e6814b4f48 Refactor packet handling to skip only the RSSI byte for improved reliability 2026-02-15 17:04:10 -08:00
Winston Lowe 1589883c88 Update lib/helpers/cayenne_lpp.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-15 16:58:44 -08:00
Winston Lowe 03b3533675 Update lib/widgets/battery_indicator.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-15 16:17:13 -08:00
Winston Lowe 608b6fb539 Update lib/screens/path_trace_map.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-15 16:16:05 -08:00
Winston Lowe 04f5c44ed9 Update lib/connector/meshcore_connector.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-15 12:59:04 -08:00
Winston Lowe 4019741a81 Update lib/connector/meshcore_connector.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-15 12:58:34 -08:00
Winston Lowe 152d5f8bb5 Update lib/models/contact.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-15 12:57:49 -08:00
Winston Lowe 3f80ae1cf7 Remove unused import from SNR indicator widget 2026-02-15 12:51:56 -08:00
Winston Lowe 246cf99415 Enhance path management dialog to display direct repeaters with color coding based on signal strength 2026-02-15 12:45:43 -08:00
Winston Lowe 5751cddaa1 Add SNRIndicator to AppBar and refactor BatteryIndicator layout 2026-02-15 11:45:48 -08:00
Winston Lowe fa5a0932ee Refactor PathTraceData to use List<double> for snrData and adjust data mapping in PathTraceMapScreen 2026-02-15 11:45:25 -08:00
Winston Lowe cdda232006 Ignore advertisements from self in MeshCoreConnector 2026-02-15 11:44:49 -08:00
Winston Lowe 63aa515f52 Fix trace route bytes generation logic in Contact model 2026-02-15 11:44:35 -08:00
Winston Lowe aed3b0157a Refactor Cayenne LPP parsing with error handling and logging
- Added error handling and logging to the Cayenne LPP parsing methods to manage malformed data gracefully.
- Improved the structure of the parsing logic for better readability and maintainability.
- Updated the Contact model to include error handling during frame parsing.
- Refactored Channels, Contacts, Map, and Neighbours screens to utilize a new AppBarTitle widget for consistent app bar design.
- Enhanced the BatteryIndicator widget to display SNR information for direct repeaters.
- Introduced SNRUi class for better management of SNR icon and text representation.
- Improved error handling in PathTraceMap and Neighbours screens to log errors appropriately.
2026-02-14 14:19:09 -08:00
49 changed files with 1510 additions and 631 deletions
+290
View File
@@ -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;
+55 -1
View File
@@ -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();
}
+19 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
} }
+22 -10
View File
@@ -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
+9 -3
View File
@@ -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 => 'Последно видян';
} }
+9 -3
View File
@@ -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';
} }
+12 -6
View File
@@ -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';
} }
+9 -3
View File
@@ -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';
} }
+9 -4
View File
@@ -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';
} }
+9 -3
View File
@@ -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';
} }
+9 -3
View File
@@ -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';
} }
+9 -3
View File
@@ -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';
} }
+9 -4
View File
@@ -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';
} }
+9 -3
View File
@@ -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 => 'Последний раз видели';
} }
+9 -3
View File
@@ -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ý';
} }
+9 -3
View File
@@ -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';
} }
+9 -3
View File
@@ -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';
} }
+9 -3
View File
@@ -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 => 'Останній раз бачили';
} }
+9 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "附近的重复器"
} }
+7 -3
View File
@@ -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
+2 -1
View File
@@ -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),
), ),
); );
} }
+48 -10
View File
@@ -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
? [ ? [
+2 -3
View File
@@ -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: [
+40 -3
View File
@@ -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),
+16 -6
View File
@@ -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(
+7 -8
View File
@@ -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),
),
],
), ),
), ),
), ),
+62 -12
View File
@@ -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
+5 -7
View File
@@ -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,
),
), ),
); );
}, },
+54
View File
@@ -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(),
],
);
}
}
+7 -3
View File
@@ -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,
), ),
), ),
], ],
), ),
],
),
), ),
); );
} }
+39 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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: