mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Merge branch 'dev' into main
This commit is contained in:
commit
2da8995d0b
54 changed files with 2896 additions and 625 deletions
|
|
@ -257,6 +257,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
int? _activeChannelIndex;
|
||||
List<int> _channelOrder = [];
|
||||
|
||||
int _storageUsedKb = -1;
|
||||
int _storageTotalKb = -1;
|
||||
|
||||
// Getters
|
||||
MeshCoreConnectionState get state => _state;
|
||||
BluetoothDevice? get device => _device;
|
||||
|
|
@ -329,10 +332,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
bool? get autoAddRoomServers => _autoAddRoomServers;
|
||||
bool? get autoAddSensors => _autoAddSensors;
|
||||
bool? get autoAddOverwriteOldest => _overwriteOldest;
|
||||
int get telemetryModeBase => _telemetryModeBase;
|
||||
int get telemetryModeLoc => _telemetryModeLoc;
|
||||
int get telemetryModeEnv => _telemetryModeEnv;
|
||||
int get advertLocationPolicy => _advertLocPolicy;
|
||||
int get multiAcks => _multiAcks;
|
||||
bool? get clientRepeat => _clientRepeat;
|
||||
int? get firmwareVerCode => _firmwareVerCode;
|
||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||
int? get batteryMillivolts => _batteryMillivolts;
|
||||
int? get storageUsedKb => _storageUsedKb;
|
||||
int? get storageTotalKb => _storageTotalKb;
|
||||
int get maxContacts => _maxContacts;
|
||||
int get maxChannels => _maxChannels;
|
||||
Set<String> get knownContactKeys => Set.unmodifiable(_knownContactKeys);
|
||||
|
|
@ -1922,13 +1932,36 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> setContactFavorite(Contact contact, bool isFavorite) async {
|
||||
Future<void> setContactFlags(
|
||||
Contact contact, {
|
||||
bool? isFavorite,
|
||||
bool? teleBase,
|
||||
bool? teleLoc,
|
||||
bool? teleEnv,
|
||||
}) async {
|
||||
if (!isConnected) return;
|
||||
final latestContact =
|
||||
await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact;
|
||||
final updatedFlags = isFavorite
|
||||
? (latestContact.flags | contactFlagFavorite)
|
||||
: (latestContact.flags & ~contactFlagFavorite);
|
||||
int updatedFlags = isFavorite != null
|
||||
? (isFavorite
|
||||
? (latestContact.flags | contactFlagFavorite)
|
||||
: (latestContact.flags & ~contactFlagFavorite))
|
||||
: latestContact.flags;
|
||||
updatedFlags = teleBase != null
|
||||
? (teleBase
|
||||
? (updatedFlags | contactFlagTeleBase)
|
||||
: (updatedFlags & ~contactFlagTeleBase))
|
||||
: updatedFlags;
|
||||
updatedFlags = teleLoc != null
|
||||
? (teleLoc
|
||||
? (updatedFlags | contactFlagTeleLoc)
|
||||
: (updatedFlags & ~contactFlagTeleLoc))
|
||||
: updatedFlags;
|
||||
updatedFlags = teleEnv != null
|
||||
? (teleEnv
|
||||
? (updatedFlags | contactFlagTeleEnv)
|
||||
: (updatedFlags & ~contactFlagTeleEnv))
|
||||
: updatedFlags;
|
||||
|
||||
await sendFrame(
|
||||
buildUpdateContactPathFrame(
|
||||
|
|
@ -2094,9 +2127,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
outboundText,
|
||||
selfKey,
|
||||
);
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
final messageBytes = utf8.encode(outboundText).length;
|
||||
_pendingRepeaterAcks[ackHashHex]?.timeout?.cancel();
|
||||
_pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext(
|
||||
|
|
@ -2468,6 +2499,31 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
|
||||
}
|
||||
|
||||
Future<void> setTelemetryModeBase(
|
||||
int base,
|
||||
int location,
|
||||
int env,
|
||||
int advert,
|
||||
int multiAcks,
|
||||
) async {
|
||||
if (!isConnected) return;
|
||||
_telemetryModeBase = base.clamp(teleModeDeny, teleModeAllowAll).toInt();
|
||||
_telemetryModeLoc = location.clamp(teleModeDeny, teleModeAllowAll).toInt();
|
||||
_telemetryModeEnv = env.clamp(teleModeDeny, teleModeAllowAll).toInt();
|
||||
_advertLocPolicy = advert.clamp(0, 1).toInt();
|
||||
_multiAcks = multiAcks.clamp(0, 2).toInt();
|
||||
await sendFrame(
|
||||
buildSetOtherParamsFrame(
|
||||
(_telemetryModeEnv << 4) |
|
||||
(_telemetryModeLoc << 2) |
|
||||
_telemetryModeBase,
|
||||
_advertLocPolicy,
|
||||
_multiAcks,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> getChannels({int? maxChannels, bool force = false}) async {
|
||||
if (!isConnected) return;
|
||||
if (_isSyncingChannels) {
|
||||
|
|
@ -2843,7 +2899,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_currentSf = reader.readByte();
|
||||
_currentCr = reader.readByte();
|
||||
|
||||
_selfName = reader.readString();
|
||||
_selfName = reader.readCString();
|
||||
} catch (e) {
|
||||
_appDebugLogService?.error(
|
||||
'Error parsing SELF_INFO frame: $e',
|
||||
|
|
@ -2984,14 +3040,23 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
// [1-2] = battery_mv (uint16 LE)
|
||||
// [3-6] = storage_used_kb (uint32 LE)
|
||||
// [7-10] = storage_total_kb (uint32 LE)
|
||||
if (frame.length >= 3) {
|
||||
_batteryMillivolts = readUint16LE(frame, 1);
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(1);
|
||||
_batteryMillivolts = reader.readUInt16LE();
|
||||
_storageUsedKb = reader.readUInt32LE();
|
||||
_storageTotalKb = reader.readUInt32LE();
|
||||
final volts = (_batteryMillivolts! / 1000.0).toStringAsFixed(2);
|
||||
_appDebugLogService?.info(
|
||||
'Pulled battery: $volts V ($_batteryMillivolts mV)',
|
||||
tag: 'Battery',
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_appDebugLogService?.error(
|
||||
'Error parsing battery and storage frame: $e',
|
||||
tag: 'Connector',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3487,7 +3552,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants
|
||||
}
|
||||
|
||||
final msgText = reader.readString();
|
||||
final msgText = reader.readCString();
|
||||
|
||||
final flags = txtType;
|
||||
final shiftedType = flags >> 2;
|
||||
|
|
@ -3649,68 +3714,87 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
|
||||
void _handleLogRxData(Uint8List frame) {
|
||||
if (frame.length < 4) return;
|
||||
final raw = Uint8List.fromList(frame.sublist(3));
|
||||
final packet = _parseRawPacket(raw);
|
||||
if (packet == null || packet.payloadType != _payloadTypeGroupText) return;
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(3); // Skip header
|
||||
|
||||
final payload = packet.payload;
|
||||
if (payload.length <= _cipherMacSize) return;
|
||||
final channelHash = payload[0];
|
||||
final encrypted = Uint8List.fromList(payload.sublist(1));
|
||||
final raw = reader.readRemainingBytes();
|
||||
final packet = _parseRawPacket(raw);
|
||||
if (packet == null || packet.payloadType != _payloadTypeGroupText) return;
|
||||
|
||||
// Use cached channels as fallback if live channels not yet loaded
|
||||
final channelsToSearch = _channels.isNotEmpty ? _channels : _cachedChannels;
|
||||
for (final channel in channelsToSearch) {
|
||||
if (channel.isEmpty) continue;
|
||||
final hash = _computeChannelHash(channel.psk);
|
||||
if (hash != channelHash) continue;
|
||||
final payload = BufferReader(packet.payload);
|
||||
final channelHash = payload.readByte();
|
||||
final encrypted = Uint8List.fromList(payload.readRemainingBytes());
|
||||
|
||||
final decrypted = _decryptPayload(channel.psk, encrypted);
|
||||
if (decrypted == null || decrypted.length < 6) return;
|
||||
// Use cached channels as fallback if live channels not yet loaded
|
||||
final channelsToSearch = _channels.isNotEmpty
|
||||
? _channels
|
||||
: _cachedChannels;
|
||||
for (final channel in channelsToSearch) {
|
||||
if (channel.isEmpty) continue;
|
||||
final hash = _computeChannelHash(channel.psk);
|
||||
if (hash != channelHash) continue;
|
||||
try {
|
||||
final decryptedBytes = _decryptPayload(channel.psk, encrypted);
|
||||
if (decryptedBytes == null || decryptedBytes.length < 6) return;
|
||||
final decrypted = BufferReader(decryptedBytes);
|
||||
|
||||
final txtType = decrypted[4];
|
||||
if ((txtType >> 2) != 0) {
|
||||
return;
|
||||
final timestampRaw = decrypted.readUInt32LE();
|
||||
final txtType = decrypted.readByte();
|
||||
if ((txtType >> 2) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final text = decrypted.readCString();
|
||||
final parsed = _splitSenderText(text);
|
||||
final decodedText =
|
||||
Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text;
|
||||
if (_shouldDropSelfChannelMessage(
|
||||
parsed.senderName,
|
||||
packet.pathBytes,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pktHash = _computePacketHash(
|
||||
packet.payloadType,
|
||||
packet.payload,
|
||||
);
|
||||
|
||||
final message = ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: parsed.senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: packet.isFlood ? packet.hopCount : 0,
|
||||
pathBytes: packet.pathBytes,
|
||||
channelIndex: channel.index,
|
||||
packetHash: pktHash,
|
||||
);
|
||||
|
||||
_updateContactLastMessageAtByName(
|
||||
parsed.senderName,
|
||||
message.timestamp,
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(channel.index, message);
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
final label = channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name;
|
||||
_maybeNotifyChannelMessage(message, channelName: label);
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
appLogger.warn('Decryption failed for channel ${channel.index}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final timestampRaw = readUint32LE(decrypted, 0);
|
||||
final text = readCString(decrypted, 5, decrypted.length - 5);
|
||||
final parsed = _splitSenderText(text);
|
||||
final decodedText = Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text;
|
||||
if (_shouldDropSelfChannelMessage(parsed.senderName, packet.pathBytes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pktHash = _computePacketHash(packet.payloadType, packet.payload);
|
||||
|
||||
final message = ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: parsed.senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: packet.isFlood ? packet.hopCount : 0,
|
||||
pathBytes: packet.pathBytes,
|
||||
channelIndex: channel.index,
|
||||
packetHash: pktHash,
|
||||
);
|
||||
|
||||
_updateContactLastMessageAtByName(
|
||||
parsed.senderName,
|
||||
message.timestamp,
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(channel.index, message);
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
final label = channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name;
|
||||
_maybeNotifyChannelMessage(message, channelName: label);
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
appLogger.warn('Error handling log RX data frame: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3721,15 +3805,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
// [2-5] = expected_ack_hash (uint32)
|
||||
// [6-9] = estimated_timeout_ms (uint32)
|
||||
|
||||
if (frame.length >= 10) {
|
||||
final ackHash = Uint8List.fromList(frame.sublist(2, 6));
|
||||
final timeoutMs = readUint32LE(frame, 6);
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(2); //Skip code and is_flood
|
||||
final ackHash = reader.readUInt32LE();
|
||||
final timeoutMs = reader.readUInt32LE();
|
||||
|
||||
// Check if this is a CLI command ACK - if so, ignore it
|
||||
if (_lastSentWasCliCommand) {
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
debugPrint('Ignoring CLI command ACK (sent): $ackHashHex');
|
||||
_lastSentWasCliCommand = false;
|
||||
return;
|
||||
|
|
@ -3748,7 +3832,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
if (_markNextPendingChannelMessageSent()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
} catch (e) {
|
||||
appLogger.warn('Error handling message sent frame: $e');
|
||||
// Fallback to old behavior
|
||||
for (var messages in _conversations.values) {
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
|
|
@ -3827,9 +3912,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
// [1-4] = ack_hash (uint32)
|
||||
// [5-8] = trip_time_ms (uint32)
|
||||
|
||||
if (frame.length >= 9) {
|
||||
final ackHash = Uint8List.fromList(frame.sublist(1, 5));
|
||||
final tripTimeMs = readUint32LE(frame, 5);
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(1); // Skip code
|
||||
final ackHash = reader.readUInt32LE();
|
||||
final tripTimeMs = reader.readUInt32LE();
|
||||
|
||||
// CLI command ACKs are already filtered in _handleMessageSent, so this should only see real messages
|
||||
|
||||
|
|
@ -3841,7 +3928,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
if (_retryService != null) {
|
||||
_retryService!.handleAckReceived(ackHash, tripTimeMs);
|
||||
}
|
||||
} else {
|
||||
} catch (e) {
|
||||
appLogger.warn('Error handling send confirmed frame: $e');
|
||||
// Fallback to old behavior
|
||||
for (var messages in _conversations.values) {
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
|
|
@ -3856,10 +3944,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) {
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
bool _handleRepeaterCommandSent(int ackHash, int timeoutMs) {
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
final entry = _pendingRepeaterAcks[ackHashHex];
|
||||
if (entry == null) return false;
|
||||
|
||||
|
|
@ -3877,10 +3963,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) {
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
bool _handleRepeaterCommandAck(int ackHash, int tripTimeMs) {
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
final entry = _pendingRepeaterAcks.remove(ackHashHex);
|
||||
if (entry == null) return false;
|
||||
entry.timeout?.cancel();
|
||||
|
|
@ -4231,36 +4315,35 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
|
||||
_RawPacket? _parseRawPacket(Uint8List raw) {
|
||||
if (raw.length < 3) return null;
|
||||
var index = 0;
|
||||
final header = raw[index++];
|
||||
final routeType = header & _phRouteMask;
|
||||
final hasTransport =
|
||||
routeType == _routeTransportFlood || routeType == _routeTransportDirect;
|
||||
if (hasTransport) {
|
||||
if (raw.length < index + 4) return null;
|
||||
index += 4;
|
||||
}
|
||||
if (raw.length <= index) return null;
|
||||
final pathLenRaw = raw[index++];
|
||||
final pathByteLen = _decodePathByteLen(pathLenRaw);
|
||||
if (raw.length < index + pathByteLen) return null;
|
||||
final pathBytes = Uint8List.fromList(
|
||||
raw.sublist(index, index + pathByteLen),
|
||||
);
|
||||
index += pathByteLen;
|
||||
if (raw.length <= index) return null;
|
||||
final payload = Uint8List.fromList(raw.sublist(index));
|
||||
try {
|
||||
final reader = BufferReader(raw);
|
||||
final header = reader.readByte();
|
||||
final routeType = header & _phRouteMask;
|
||||
final hasTransport =
|
||||
routeType == _routeTransportFlood ||
|
||||
routeType == _routeTransportDirect;
|
||||
if (hasTransport) {
|
||||
// Skip reserved bytes in transport header made up of two u16 fields
|
||||
reader.skipBytes(4);
|
||||
}
|
||||
final pathLenRaw = reader.readByte();
|
||||
final pathByteLen = _decodePathByteLen(pathLenRaw);
|
||||
final pathBytes = reader.readBytes(pathByteLen);
|
||||
final payload = reader.readBytes(reader.remaining);
|
||||
|
||||
return _RawPacket(
|
||||
header: header,
|
||||
routeType: routeType,
|
||||
payloadType: (header >> _phTypeShift) & _phTypeMask,
|
||||
payloadVer: (header >> _phVerShift) & _phVerMask,
|
||||
pathLenRaw: pathLenRaw,
|
||||
pathBytes: pathBytes,
|
||||
payload: payload,
|
||||
);
|
||||
return _RawPacket(
|
||||
header: header,
|
||||
routeType: routeType,
|
||||
payloadType: (header >> _phTypeShift) & _phTypeMask,
|
||||
payloadVer: (header >> _phVerShift) & _phVerMask,
|
||||
pathLenRaw: pathLenRaw,
|
||||
pathBytes: pathBytes,
|
||||
payload: payload,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.warn('Error parsing raw packet: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int _computeChannelHash(Uint8List psk) {
|
||||
|
|
@ -4679,7 +4762,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
void _handleCustomVars(Uint8List frame) {
|
||||
final buf = BufferReader(frame.sublist(1));
|
||||
try {
|
||||
_currentCustomVars = _parseKeyValueString(buf.readString());
|
||||
_currentCustomVars = _parseKeyValueString(buf.readCString());
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
|
||||
}
|
||||
|
|
@ -4836,7 +4919,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
longitude = packet.readInt32LE() / 1e6;
|
||||
}
|
||||
if (hasName && packet.remaining > 0) {
|
||||
name = packet.readString();
|
||||
name = packet.readCString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
|
|
@ -4898,7 +4981,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
longitude = advert.readInt32LE() / 1e6;
|
||||
}
|
||||
if (hasName && advert.remaining > 0) {
|
||||
name = advert.readString();
|
||||
name = advert.readCString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
|
|
@ -5124,6 +5207,25 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
unawaited(_persistDiscoveredContacts());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearMessagesForContact(Contact contact) {
|
||||
final contactKeyHex = contact.publicKeyHex;
|
||||
final messages = _conversations[contactKeyHex];
|
||||
if (messages == null) return;
|
||||
messages.clear();
|
||||
unawaited(_messageStore.saveMessages(contactKeyHex, messages));
|
||||
markContactRead(contactKeyHex);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearMessagesForChannel(int channelIndex) {
|
||||
final messages = _channelMessages[channelIndex];
|
||||
if (messages == null) return;
|
||||
messages.clear();
|
||||
unawaited(_channelMessageStore.saveChannelMessages(channelIndex, messages));
|
||||
markChannelRead(channelIndex);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
const int _phRouteMask = 0x03;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||
class BufferReader {
|
||||
int _pointer = 0;
|
||||
|
|
@ -37,16 +39,6 @@ class BufferReader {
|
|||
|
||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||
|
||||
String readString() {
|
||||
_lastPointer = _pointer;
|
||||
final value = readRemainingBytes();
|
||||
try {
|
||||
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||
} catch (e) {
|
||||
return String.fromCharCodes(value); // Latin-1 fallback
|
||||
}
|
||||
}
|
||||
|
||||
String readCStringGreedy(int maxLength) {
|
||||
_lastPointer = _pointer;
|
||||
final value = <int>[];
|
||||
|
|
@ -62,11 +54,12 @@ class BufferReader {
|
|||
}
|
||||
}
|
||||
|
||||
String readCString(int maxLength) {
|
||||
String readCString({int maxLength = -1}) {
|
||||
final backupPointer = _pointer;
|
||||
final value = <int>[];
|
||||
int counter = 0;
|
||||
while (counter < maxLength) {
|
||||
final maxLen = maxLength >= 0 ? maxLength : remaining;
|
||||
while (counter < maxLen) {
|
||||
final byte = readByte();
|
||||
if (byte == 0) break;
|
||||
value.add(byte);
|
||||
|
|
@ -210,7 +203,7 @@ const int cmdSetChannel = 32;
|
|||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdSendTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
|
|
@ -220,6 +213,7 @@ const int cmdGetAutoAddConfig = 59;
|
|||
// Text message types
|
||||
const int txtTypePlain = 0;
|
||||
const int txtTypeCliData = 1;
|
||||
const int txtTypeSigned = 2;
|
||||
|
||||
// Repeater request types (for server requests)
|
||||
const int reqTypeGetStatus = 0x01;
|
||||
|
|
@ -272,6 +266,10 @@ const int advTypeRepeater = 2;
|
|||
const int advTypeRoom = 3;
|
||||
const int advTypeSensor = 4;
|
||||
|
||||
const int teleModeDeny = 0;
|
||||
const int teleModeAllowFlags = 1; // use contact.flags
|
||||
const int teleModeAllowAll = 2;
|
||||
|
||||
// Payload Types
|
||||
const int payloadTypeREQ =
|
||||
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
|
|
@ -310,6 +308,7 @@ const int autoAddSensorFlag =
|
|||
|
||||
// Sizes
|
||||
const int pubKeySize = 32;
|
||||
const int signatureSize = 64;
|
||||
const int maxPathSize = 64;
|
||||
const int pathHashSize = 1;
|
||||
const int maxNameSize = 32;
|
||||
|
|
@ -352,6 +351,9 @@ const int contactPubKeyOffset = 1;
|
|||
const int contactTypeOffset = 33;
|
||||
const int contactFlagsOffset = 34;
|
||||
const int contactFlagFavorite = 0x01;
|
||||
const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
|
||||
const int contactFlagTeleLoc = 0x04;
|
||||
const int contactFlagTeleEnv = 0x08; //access environment sensors
|
||||
const int contactPathLenOffset = 35;
|
||||
const int contactPathOffset = 36;
|
||||
const int contactNameOffset = 100;
|
||||
|
|
@ -370,52 +372,44 @@ const int msgTextOffset = 38;
|
|||
class ParsedContactText {
|
||||
final Uint8List senderPrefix;
|
||||
final String text;
|
||||
|
||||
const ParsedContactText({required this.senderPrefix, required this.text});
|
||||
}
|
||||
|
||||
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
||||
if (frame.isEmpty) return null;
|
||||
final code = frame[0];
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
|
||||
final message = BufferReader(frame);
|
||||
try {
|
||||
final code = message.readByte();
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Companion radio layout:
|
||||
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
||||
if (code == respCodeContactMsgRecvV3) {
|
||||
// Skip SNR and reserved bytes in v3 layout
|
||||
message.skipBytes(3);
|
||||
}
|
||||
final senderPrefix = message.readBytes(6); // public key
|
||||
message.skipBytes(1); // path length
|
||||
final textType = message.readByte();
|
||||
message.skipBytes(4); // timestamp (4 bytes)
|
||||
|
||||
final shiftedType = textType >> 2;
|
||||
final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
|
||||
if (isSigned) {
|
||||
// Signed messages have a 4-byte signature after the timestamp, before the text
|
||||
message.skipBytes(4);
|
||||
}
|
||||
final text = message.readCString();
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing contact message text: $e');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Companion radio layout:
|
||||
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
||||
final isV3 = code == respCodeContactMsgRecvV3;
|
||||
final prefixOffset = isV3 ? 4 : 1;
|
||||
const prefixLen = 6;
|
||||
final txtTypeOffset = prefixOffset + prefixLen + 1;
|
||||
final timestampOffset = txtTypeOffset + 1;
|
||||
final baseTextOffset = timestampOffset + 4;
|
||||
if (frame.length <= baseTextOffset) return null;
|
||||
|
||||
final flags = frame[txtTypeOffset];
|
||||
final shiftedType = flags >> 2;
|
||||
final rawType = flags;
|
||||
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
|
||||
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
|
||||
if (!isPlain && !isCli) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var text = readCString(
|
||||
frame,
|
||||
baseTextOffset,
|
||||
frame.length - baseTextOffset,
|
||||
).trim();
|
||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
||||
text = readCString(
|
||||
frame,
|
||||
baseTextOffset + 4,
|
||||
frame.length - (baseTextOffset + 4),
|
||||
).trim();
|
||||
}
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
|
||||
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
||||
}
|
||||
|
||||
// Helper to read uint32 little-endian
|
||||
|
|
@ -438,18 +432,9 @@ int readInt32LE(Uint8List data, int offset) {
|
|||
return val;
|
||||
}
|
||||
|
||||
// Helper to read null-terminated UTF-8 string
|
||||
String readCString(Uint8List data, int offset, int maxLen) {
|
||||
int end = offset;
|
||||
while (end < offset + maxLen && end < data.length && data[end] != 0) {
|
||||
end++;
|
||||
}
|
||||
try {
|
||||
return utf8.decode(data.sublist(offset, end), allowMalformed: true);
|
||||
} catch (e) {
|
||||
// Fallback to Latin-1 if UTF-8 decoding fails
|
||||
return String.fromCharCodes(data.sublist(offset, end));
|
||||
}
|
||||
// Helper to convert uint32 to hex string
|
||||
String ackHashToHex(int ackHash) {
|
||||
return ackHash.toRadixString(16).padLeft(8, '0');
|
||||
}
|
||||
|
||||
// Helper to convert public key to hex string
|
||||
|
|
@ -937,3 +922,18 @@ Uint8List buildSetAutoAddConfigFrame({
|
|||
writer.writeByte(flags);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
//Build CMD_SEND_TELEMETRY_REQ
|
||||
// Format: [cmd][reserved x3][pub_key? x32]
|
||||
Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTelemetryReq);
|
||||
|
||||
if (pubKey != null && pubKey.length == pubKeySize) {
|
||||
writer.writeBytes(Uint8List(3)); // reserved bytes
|
||||
writer.writeBytes(pubKey);
|
||||
} else {
|
||||
writer.writeBytes(Uint8List(4)); // reserved bytes
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_denyAll": "Откажи всичко",
|
||||
"settings_allowAll": "Позволи всичко",
|
||||
"settings_allowByContact": "Позволи по флагове за контакт",
|
||||
"settings_privacy": "Настройки на поверителността",
|
||||
"settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.",
|
||||
"settings_privacySubtitle": "Контролирайте каква информация се споделя.",
|
||||
"settings_telemetryBaseMode": "Базов режим на телеметрия",
|
||||
"settings_telemetryLocationMode": "Режим на местоположение на телеметрията",
|
||||
"settings_advertLocation": "Място на обявата",
|
||||
"settings_advertLocationSubtitle": "Включи местоположение в обявата",
|
||||
"contact_info": "Контактна информация",
|
||||
"settings_telemetryEnvironmentMode": "Режим на средата на телеметрията",
|
||||
"contact_telemetry": "Телеметрия",
|
||||
"contact_lastSeen": "Последно видян",
|
||||
"contact_clearChat": "Изчисти чата",
|
||||
"contact_teleBase": "Базата данни за телеметрия",
|
||||
"contact_settings": "Настройки за контакти",
|
||||
"contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия",
|
||||
"contact_teleEnv": "Среда на телеметрия",
|
||||
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
|
||||
"contact_teleLoc": "Местоположение на телеметрията",
|
||||
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
|
||||
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.",
|
||||
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Мулти-потвърди: {value}",
|
||||
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен"
|
||||
}
|
||||
|
|
@ -1928,6 +1928,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_allowByContact": "Zulassen durch Kontaktflaggen",
|
||||
"settings_privacy": "Datenschutzeinstellungen",
|
||||
"settings_allowAll": "Alles zulassen",
|
||||
"settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.",
|
||||
"settings_denyAll": "Alle ablehnen",
|
||||
"settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.",
|
||||
"settings_telemetryLocationMode": "Telemetrie-Ortsmodus",
|
||||
"settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus",
|
||||
"settings_advertLocation": "Anzeigenort",
|
||||
"settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen",
|
||||
"settings_telemetryBaseMode": "Telemetrie-Basismodus",
|
||||
"contact_teleBase": "Telemetriebasis",
|
||||
"contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie",
|
||||
"contact_teleLoc": "Telemetrieort",
|
||||
"contact_teleLocSubtitle": "Teilen von Standortdaten zulassen",
|
||||
"contact_info": "Kontaktinformationen",
|
||||
"contact_settings": "Kontakteinstellungen",
|
||||
"contact_telemetry": "Telemetrie",
|
||||
"contact_teleEnv": "Telemetrieumgebung",
|
||||
"contact_lastSeen": "Zuletzt gesehen",
|
||||
"contact_clearChat": "Chat löschen",
|
||||
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
|
||||
"appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
|
||||
|
|
@ -1938,5 +1967,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde",
|
||||
"appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
|
||||
"settings_multiAck": "Mehrfach-Bestätigungen: {value}"
|
||||
}
|
||||
|
|
@ -166,6 +166,26 @@
|
|||
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
|
||||
"settings_privacyModeEnabled": "Privacy mode enabled",
|
||||
"settings_privacyModeDisabled": "Privacy mode disabled",
|
||||
"settings_privacy": "Privacy Settings",
|
||||
"settings_privacySubtitle": "Control what information is shared.",
|
||||
"settings_privacySettingsDescription": "Choose what information your device shares with others.",
|
||||
"settings_denyAll": "Deny all",
|
||||
"settings_allowByContact": "Allow by contact flags",
|
||||
"settings_allowAll": "Allow all",
|
||||
"settings_telemetryBaseMode": "Telemetry Base Mode",
|
||||
"settings_telemetryLocationMode": "Telemetry Location Mode",
|
||||
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
|
||||
"settings_advertLocation": "Advert Location",
|
||||
"settings_advertLocationSubtitle": "Include location in advert.",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Telemetry mode updated",
|
||||
"settings_actions": "Actions",
|
||||
"settings_sendAdvertisement": "Send Advertisement",
|
||||
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
|
||||
|
|
@ -472,6 +492,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"contact_info": "Contact Info",
|
||||
"contact_settings": "Contact Settings",
|
||||
"contact_telemetry": "Telemetry",
|
||||
"contact_lastSeen": "Last seen",
|
||||
"contact_clearChat": "Clear Chat",
|
||||
"contact_teleBase": "Telemetry Base",
|
||||
"contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry",
|
||||
"contact_teleLoc": "Telemetry Location",
|
||||
"contact_teleLocSubtitle": "Allow sharing location data",
|
||||
"contact_teleEnv": "Telemetry Environment",
|
||||
"contact_teleEnvSubtitle": "Allow sharing environment sensor data",
|
||||
"channels_title": "Channels",
|
||||
"channels_noChannelsConfigured": "No channels configured",
|
||||
"channels_addPublicChannel": "Add Public Channel",
|
||||
|
|
@ -1945,4 +1976,4 @@
|
|||
"discoveredContacts_deleteContact": "Delete Discovered Contact",
|
||||
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
|
||||
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?"
|
||||
}
|
||||
}
|
||||
|
|
@ -1928,6 +1928,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacySubtitle": "Controlar qué información se comparte.",
|
||||
"settings_allowByContact": "Permitir por banderas de contacto",
|
||||
"settings_denyAll": "Denegar todo",
|
||||
"settings_telemetryBaseMode": "Modo base de telemetría",
|
||||
"settings_telemetryEnvironmentMode": "Modo de entorno de telemetría",
|
||||
"settings_advertLocationSubtitle": "Incluir ubicación en anuncio",
|
||||
"contact_info": "Información de contacto",
|
||||
"settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.",
|
||||
"settings_allowAll": "Permitir todo",
|
||||
"settings_privacy": "Configuración de privacidad",
|
||||
"contact_settings": "Configuración de contacto",
|
||||
"settings_telemetryLocationMode": "Modo de ubicación de telemetría",
|
||||
"contact_teleBase": "Base de Telemetría",
|
||||
"contact_teleLoc": "Ubicación de telemetría",
|
||||
"settings_advertLocation": "Ubicación de anuncio",
|
||||
"contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación",
|
||||
"contact_clearChat": "Borrar chat",
|
||||
"contact_telemetry": "Telemetría",
|
||||
"contact_lastSeen": "Visto por última vez",
|
||||
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
|
||||
"contact_teleEnv": "Entorno de Telemetría",
|
||||
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso inicial de la ruta",
|
||||
"appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
|
||||
|
|
@ -1938,5 +1967,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.",
|
||||
"appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
}
|
||||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "Paramètres de confidentialité",
|
||||
"settings_privacySubtitle": "Contrôlez les informations partagées",
|
||||
"settings_telemetryLocationMode": "Mode d'emplacement de télémétrie",
|
||||
"settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie",
|
||||
"settings_advertLocation": "Emplacement de l'annonce",
|
||||
"settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce",
|
||||
"settings_denyAll": "Refuser tout",
|
||||
"settings_allowByContact": "Autoriser par drapeaux de contact",
|
||||
"settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.",
|
||||
"settings_allowAll": "Autoriser tout",
|
||||
"contact_info": "Informations de contact",
|
||||
"settings_telemetryBaseMode": "Mode de base Télémétrie",
|
||||
"contact_teleBase": "Base de télémétrie",
|
||||
"contact_teleLoc": "Emplacement de télémétrie",
|
||||
"contact_teleLocSubtitle": "Autoriser le partage des données de localisation",
|
||||
"contact_teleEnv": "Environnement Télémétrie",
|
||||
"contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement",
|
||||
"contact_telemetry": "Télémétrie",
|
||||
"contact_settings": "Paramètres de contact",
|
||||
"contact_lastSeen": "Dernière fois vu",
|
||||
"contact_clearChat": "Effacer la conversation",
|
||||
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.",
|
||||
"appSettings_initialRouteWeight": "Poids initial de l'itinéraire",
|
||||
"appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.",
|
||||
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Multi-ACKs : {value}",
|
||||
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour"
|
||||
}
|
||||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.",
|
||||
"settings_allowByContact": "Consenti in base ai flag di contatto",
|
||||
"settings_telemetryLocationMode": "Modalità di posizionamento telemetrico",
|
||||
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
|
||||
"settings_advertLocation": "Posizione dell'annuncio",
|
||||
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
|
||||
"settings_privacy": "Impostazioni sulla privacy",
|
||||
"settings_denyAll": "Negare tutto",
|
||||
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
|
||||
"settings_allowAll": "Consenti tutto",
|
||||
"contact_info": "Informazioni di Contatto",
|
||||
"settings_telemetryBaseMode": "Modalità di base di telemetria",
|
||||
"contact_teleBase": "Base di telemetria",
|
||||
"contact_teleLoc": "Posizione telemetria",
|
||||
"contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione",
|
||||
"contact_clearChat": "Cancella chat",
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_settings": "Impostazioni di contatto",
|
||||
"contact_lastSeen": "Ultimo accesso",
|
||||
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
|
||||
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
|
||||
"contact_teleEnv": "Ambiente di telemetria",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso iniziale del percorso",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
|
||||
"appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.",
|
||||
"appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
}
|
||||
|
|
@ -826,6 +826,84 @@ abstract class AppLocalizations {
|
|||
/// **'Privacy mode disabled'**
|
||||
String get settings_privacyModeDisabled;
|
||||
|
||||
/// No description provided for @settings_privacy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Privacy Settings'**
|
||||
String get settings_privacy;
|
||||
|
||||
/// No description provided for @settings_privacySubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Control what information is shared.'**
|
||||
String get settings_privacySubtitle;
|
||||
|
||||
/// No description provided for @settings_privacySettingsDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose what information your device shares with others.'**
|
||||
String get settings_privacySettingsDescription;
|
||||
|
||||
/// No description provided for @settings_denyAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Deny all'**
|
||||
String get settings_denyAll;
|
||||
|
||||
/// No description provided for @settings_allowByContact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow by contact flags'**
|
||||
String get settings_allowByContact;
|
||||
|
||||
/// No description provided for @settings_allowAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow all'**
|
||||
String get settings_allowAll;
|
||||
|
||||
/// No description provided for @settings_telemetryBaseMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Base Mode'**
|
||||
String get settings_telemetryBaseMode;
|
||||
|
||||
/// No description provided for @settings_telemetryLocationMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Location Mode'**
|
||||
String get settings_telemetryLocationMode;
|
||||
|
||||
/// No description provided for @settings_telemetryEnvironmentMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Environment Mode'**
|
||||
String get settings_telemetryEnvironmentMode;
|
||||
|
||||
/// No description provided for @settings_advertLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Advert Location'**
|
||||
String get settings_advertLocation;
|
||||
|
||||
/// No description provided for @settings_advertLocationSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Include location in advert.'**
|
||||
String get settings_advertLocationSubtitle;
|
||||
|
||||
/// No description provided for @settings_multiAck.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Multi-ACKs: {value}'**
|
||||
String settings_multiAck(String value);
|
||||
|
||||
/// No description provided for @settings_telemetryModeUpdated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry mode updated'**
|
||||
String get settings_telemetryModeUpdated;
|
||||
|
||||
/// No description provided for @settings_actions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1846,6 +1924,72 @@ abstract class AppLocalizations {
|
|||
/// **'~ {days} days'**
|
||||
String contacts_lastSeenDaysAgo(int days);
|
||||
|
||||
/// No description provided for @contact_info.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact Info'**
|
||||
String get contact_info;
|
||||
|
||||
/// No description provided for @contact_settings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact Settings'**
|
||||
String get contact_settings;
|
||||
|
||||
/// No description provided for @contact_telemetry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry'**
|
||||
String get contact_telemetry;
|
||||
|
||||
/// No description provided for @contact_lastSeen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last seen'**
|
||||
String get contact_lastSeen;
|
||||
|
||||
/// No description provided for @contact_clearChat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear Chat'**
|
||||
String get contact_clearChat;
|
||||
|
||||
/// No description provided for @contact_teleBase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Base'**
|
||||
String get contact_teleBase;
|
||||
|
||||
/// No description provided for @contact_teleBaseSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow sharing battery level and basic telemetry'**
|
||||
String get contact_teleBaseSubtitle;
|
||||
|
||||
/// No description provided for @contact_teleLoc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Location'**
|
||||
String get contact_teleLoc;
|
||||
|
||||
/// No description provided for @contact_teleLocSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow sharing location data'**
|
||||
String get contact_teleLocSubtitle;
|
||||
|
||||
/// No description provided for @contact_teleEnv.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Environment'**
|
||||
String get contact_teleEnv;
|
||||
|
||||
/// No description provided for @contact_teleEnvSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow sharing environment sensor data'**
|
||||
String get contact_teleEnvSubtitle;
|
||||
|
||||
/// No description provided for @channels_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -398,6 +398,52 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
String get settings_privacyModeDisabled =>
|
||||
'Режим на поверителност е деактивиран';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Настройки на поверителността';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Контролирайте каква информация се споделя.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Изберете каква информация устройството ви споделя с другите.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Откажи всичко';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Позволи по флагове за контакт';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Позволи всичко';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Базов режим на телеметрия';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Режим на местоположение на телеметрията';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Режим на средата на телеметрията';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Място на обявата';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включи местоположение в обявата';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мулти-потвърди: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
|
|
@ -989,6 +1035,42 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
return 'Последно видян $days дни преди.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Контактна информация';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Настройки за контакти';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Последно видян';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Изчисти чата';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Базата данни за телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Позволи споделяне на ниво на батерията и основна телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Местоположение на телеметрията';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Позволи споделяне на данни за местоположение';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Среда на телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Позволи споделяне на данни от средносферните датчици';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Канали';
|
||||
|
||||
|
|
|
|||
|
|
@ -398,6 +398,50 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Datenschutzeinstellungen';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Steuern Sie die Informationen, die freigegeben werden.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Alle ablehnen';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Zulassen durch Kontaktflaggen';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Alles zulassen';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetrie-Basismodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Anzeigenort';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Ort in der Anzeige einbeziehen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Mehrfach-Bestätigungen: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Aktionen';
|
||||
|
||||
|
|
@ -987,6 +1031,41 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return '~ $days Tage';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Kontaktinformationen';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Kontakteinstellungen';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Zuletzt gesehen';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Chat löschen';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetriebasis';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetrieort';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetrieumgebung';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Teilen von Umgebungsensordaten zulassen';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanäle';
|
||||
|
||||
|
|
|
|||
|
|
@ -392,6 +392,48 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Privacy mode disabled';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Privacy Settings';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Control what information is shared.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Choose what information your device shares with others.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Deny all';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Allow by contact flags';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Allow all';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetry Base Mode';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetry Location Mode';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Advert Location';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Include location in advert.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
|
|
@ -972,6 +1014,40 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return '~ $days days';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Contact Info';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Contact Settings';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetry';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Last seen';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Clear Chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetry Base';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Allow sharing battery level and basic telemetry';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetry Location';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Allow sharing location data';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetry Environment';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Channels';
|
||||
|
||||
|
|
|
|||
|
|
@ -396,6 +396,51 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Configuración de privacidad';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Controlar qué información se comparte.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Elige qué información comparte tu dispositivo con otros.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Denegar todo';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Permitir por banderas de contacto';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Permitir todo';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Modo base de telemetría';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Modo de ubicación de telemetría';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Modo de entorno de telemetría';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Ubicación de anuncio';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Acciones';
|
||||
|
||||
|
|
@ -987,6 +1032,42 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
return '~ $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Información de contacto';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Configuración de contacto';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Visto por última vez';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Borrar chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Permitir el intercambio de nivel de batería y telemetría básica';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Ubicación de telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Permitir el intercambio de datos de ubicación';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Entorno de Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Permitir el intercambio de datos de sensores de entorno';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canales';
|
||||
|
||||
|
|
|
|||
|
|
@ -400,6 +400,52 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get settings_privacyModeDisabled =>
|
||||
'Mode de confidentialité désactivé';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Paramètres de confidentialité';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Contrôlez les informations partagées';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Choisissez les informations que votre appareil partage avec les autres.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Refuser tout';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Autoriser par drapeaux de contact';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Autoriser tout';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Mode de base Télémétrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Mode d\'emplacement de télémétrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Mode d\'environnement de télémétrie';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Emplacement de l\'annonce';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Inclure l\'emplacement dans l\'annonce';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs : $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
'Le mode télémétrie a été mis à jour';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
|
|
@ -991,6 +1037,42 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
return '~ $days jours';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informations de contact';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Paramètres de contact';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Dernière fois vu';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Effacer la conversation';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Autoriser le partage du niveau de batterie et de la télémétrie de base';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Emplacement de télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Autoriser le partage des données de localisation';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Environnement Télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Autoriser le partage des données des capteurs d\'environnement';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canaux';
|
||||
|
||||
|
|
|
|||
|
|
@ -398,6 +398,52 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Impostazioni sulla privacy';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Controlla le informazioni che vengono condivise.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Scegli le informazioni che il tuo dispositivo condivide con gli altri.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Negare tutto';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Consenti in base ai flag di contatto';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Consenti tutto';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Modalità di base di telemetria';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Modalità di posizionamento telemetrico';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Modalità di ambiente di telemetria';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Posizione dell\'annuncio';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Includi la posizione nell\'annuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Azioni';
|
||||
|
||||
|
|
@ -987,6 +1033,42 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
return 'Ultimo visto $days giorni fa';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informazioni di Contatto';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Impostazioni di contatto';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Ultimo accesso';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Cancella chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base di telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Consenti la condivisione del livello della batteria e della telemetria di base';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Posizione telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Consenti la condivisione dei dati di posizione';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Ambiente di telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Consenti la condivisione dei dati del sensore ambientale';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canali';
|
||||
|
||||
|
|
|
|||
|
|
@ -395,6 +395,50 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Privacyinstellingen';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Beheer welke informatie wordt gedeeld';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Kies welke informatie uw apparaat deelt met anderen';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Weiger alles';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Toestaan op basis van contactvlaggen';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Alles toestaan';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetrie-basismodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetrie-locatiemodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetrie-omgevingsmodus';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Advertentielocatie';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Locatie opnemen in advertentie';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Acties';
|
||||
|
||||
|
|
@ -980,6 +1024,40 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
return 'Laast gezien $days dagen geleden';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Contactinformatie';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Contactinstellingen';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Laatst gezien';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Chat leegmaken';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetrie_basis';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Sta delen van batterij niveau en basis telemetrie toe';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetrielocatie';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Locatiegegevens delen toestaan';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetrieomgeving';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => 'Delen van omgevingsensordata toestaan';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanaal';
|
||||
|
||||
|
|
|
|||
|
|
@ -401,6 +401,52 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Ustawienia prywatności';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Kontroluj jakie informacje są udostępniane.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Wybierz jakie informacje urządzenie udostępni innym.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Odmów wszystkim';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Zezwalaj według flag kontaktowych';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Zezwalaj na wszystko';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Tryb podstawowy telemetrii';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Tryb położenia telemetrycznego';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Tryb środowiska telemetrycznego';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Lokalizacja reklamowa';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Uwzględnij lokalizację w ogłoszeniu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Wiele potwierdzeń: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
'Tryb telemetryczny zaktualizowany';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Działania';
|
||||
|
||||
|
|
@ -996,6 +1042,42 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
return '~ $days dni';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informacje kontaktowe';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Ustawienia kontaktowe';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetryka';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Ostatnio widziany';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Wyczyść czat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Baza telemetryczna';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Lokalizacja telemetryczna';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Zezwalaj na udostępnianie danych lokalizacji';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Środowisko telemetryczne';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Zezwalaj na udostępnianie danych czujników środowiskowych';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanały';
|
||||
|
||||
|
|
|
|||
|
|
@ -398,6 +398,51 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Configurações de Privacidade';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Controle o que é compartilhado.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Escolha quais informações o seu dispositivo compartilha com os outros.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Negar todos';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Permitir por bandeiras de contato';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Permitir todos';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Modo Base de Telemetria';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Modo de Localização de Telemetria';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Modo de Ambiente de Telemetria';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Localização do Anúncio';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Incluir localização no anúncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Ações';
|
||||
|
||||
|
|
@ -988,6 +1033,42 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
return 'Última vez visto $days dias atrás';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informações de Contato';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Configurações de Contato';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Visto pela última vez';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Limpar Chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Permitir compartilhamento do nível da bateria e telemetria básica';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Localização de Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Permitir compartilhamento de dados de localização';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Ambiente de Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Permitir compartilhamento de dados do sensor de ambiente';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canais';
|
||||
|
||||
|
|
|
|||
|
|
@ -398,6 +398,51 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
String get settings_privacyModeDisabled =>
|
||||
'Режим конфиденциальности выключен';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Настройки конфиденциальности';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Контролируйте, какую информацию делиться.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Выберите, какую информацию ваше устройство будет делиться с другими.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Отклонить все';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Разрешить по флагам контактов';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Разрешить все';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Базовый режим телеметрии';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Режим местоположения телеметрии';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Местоположение рекламы';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включить местоположение в объявление';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мульти-ACK: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
|
|
@ -988,6 +1033,42 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
return 'Видели $days дн. назад';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Контактная информация';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Настройки контактов';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Последний раз видели';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Очистить чат';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'База телеметрии';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Разрешить обмен уровнем заряда батареи и базовой телеметрией';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Местоположение телеметрии';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Разрешить обмен данными о местоположении';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Среда телеметрии';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Разрешить обмен данными датчиков окружающей среды';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Каналы';
|
||||
|
||||
|
|
|
|||
|
|
@ -395,6 +395,49 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Nastavenia súkromia';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Zamietnuť všetko';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Povoliť všetko';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Základný režim telemetrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Umiestnenie inzerátu';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Viaceré ACK: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
'Režim telemetrie bol aktualizovaný';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Možné akcie';
|
||||
|
||||
|
|
@ -980,6 +1023,41 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
return 'Posledné zobrazenie $days dní dozadu';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Kontaktné informácie';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Nastavenia kontaktov';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Naposledy videný';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Vymazať chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Báza telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Povoliť zdieľanie úrovne batérie a základnej telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Lokácia telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Prostredie telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Povoliť zdieľanie údajov senzorov prostredia';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanály';
|
||||
|
||||
|
|
|
|||
|
|
@ -393,6 +393,50 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Nastavitve zasebnosti';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Kontrolirajte, katere informacije so deljene.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Izberite, katere informacije vaš naprava deli z drugimi.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Zavrniti vse';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Dovoli po kontaktnih zastavah';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Dovoli vse';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Osnovni način telemetrije';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Način delovanja telemetrije';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Način delovanja okolja telemetrije';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Lokacija oglasa';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Večkratni potrditvi: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Akcije';
|
||||
|
||||
|
|
@ -977,6 +1021,41 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
return 'Zadnjič viden pred $days dnem';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Kontaktni podatki';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Nastavitve stika';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetrija';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Zadnjič videno';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Počisti klepet';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Baza telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Dovoli deljenje stanja baterije in osnovne telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Lokacija telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Okolje telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Dovoli deljenje podatkov okoljskih senzorjev';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanali';
|
||||
|
||||
|
|
|
|||
|
|
@ -392,6 +392,49 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Privatläge är avstängt';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Inställningar för sekretess';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Kontrollera vilken information som delas.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Välj vilken information din enhet delar med andra.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Neka alla';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Tillåt via kontaktflaggor';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Tillåt alla';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetribasläge';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetritillstånd för plats';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Annonsplacering';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Åtgärder';
|
||||
|
||||
|
|
@ -972,6 +1015,40 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
return 'Senast synlig $days dagar sedan';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Kontaktinformation';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Kontaktinställningar';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetri';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Senast sedd';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Rensa Chatt';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetribas';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Tillåt delning av batterinivå och grundläggande telemetri';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetridata plats';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Tillåt delning av platsdata';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetri Miljö';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanaler';
|
||||
|
||||
|
|
|
|||
|
|
@ -395,6 +395,50 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => 'Режим приватності вимкнено';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Налаштування приватності';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Керуйте інформацією, яку буде спільно використовуватися';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Виберіть, яку інформацію ваш пристрій буде передавати іншим.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Відхилити все';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Дозволити за контактними прапорцями';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Дозволити все';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Режим базової телеметрії';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Режим місця телеметрії';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Розміщення реклами';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включити місце розташування в оголошення';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Багатократне підтвердження: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Дії';
|
||||
|
||||
|
|
@ -983,6 +1027,42 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
return 'В мережі $days дн. тому';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Контактна інформація';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Налаштування контактів';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Телеметрія';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Останній раз бачили';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Очистити чат';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Базовий телебачення';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Розташування телеметрії';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Дозволити спільне використання даних про місцеположення';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Середовище телеметрії';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Дозволити спільний доступ до даних датчиків середовища';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Канали';
|
||||
|
||||
|
|
|
|||
|
|
@ -374,6 +374,47 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get settings_privacyModeDisabled => '隐私模式已关闭';
|
||||
|
||||
@override
|
||||
String get settings_privacy => '隐私设置';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => '控制要共享的信息。';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => '拒绝所有';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => '按联系人标志允许';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => '允许全部';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => '遥测基础模式';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => '遥测位置模式';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => '遥测环境模式';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => '广告位置';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '在广告中包含位置';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '多重ACK:$value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => '遥测模式已更新';
|
||||
|
||||
@override
|
||||
String get settings_actions => '操作';
|
||||
|
||||
|
|
@ -923,6 +964,39 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
return '最后在线 $days 天前';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => '联系信息';
|
||||
|
||||
@override
|
||||
String get contact_settings => '联系人设置';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => '遥测数据';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => '最近出现';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => '清除聊天记录';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => '遥测基站';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => '遥测位置';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => '允许共享位置数据';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => '遥测环境';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => '允许共享环境传感器数据';
|
||||
|
||||
@override
|
||||
String get channels_title => '频道';
|
||||
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "Privacyinstellingen",
|
||||
"settings_privacySubtitle": "Beheer welke informatie wordt gedeeld",
|
||||
"settings_telemetryLocationMode": "Telemetrie-locatiemodus",
|
||||
"settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus",
|
||||
"settings_advertLocation": "Advertentielocatie",
|
||||
"settings_advertLocationSubtitle": "Locatie opnemen in advertentie",
|
||||
"settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen",
|
||||
"settings_allowByContact": "Toestaan op basis van contactvlaggen",
|
||||
"settings_allowAll": "Alles toestaan",
|
||||
"settings_denyAll": "Weiger alles",
|
||||
"contact_info": "Contactinformatie",
|
||||
"settings_telemetryBaseMode": "Telemetrie-basismodus",
|
||||
"contact_teleBase": "Telemetrie_basis",
|
||||
"contact_teleLoc": "Telemetrielocatie",
|
||||
"contact_teleLocSubtitle": "Locatiegegevens delen toestaan",
|
||||
"contact_teleEnv": "Telemetrieomgeving",
|
||||
"contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan",
|
||||
"contact_settings": "Contactinstellingen",
|
||||
"contact_telemetry": "Telemetrie",
|
||||
"contact_lastSeen": "Laatst gezien",
|
||||
"contact_clearChat": "Chat leegmaken",
|
||||
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.",
|
||||
"appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route",
|
||||
"appSettings_maxRouteWeight": "Maximale gewicht voor de route",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering",
|
||||
"appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
}
|
||||
|
|
@ -1938,15 +1938,46 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Początkowa waga ścieżki",
|
||||
"appSettings_maxRouteWeight": "Maksymalna waga ścieżki",
|
||||
"appSettings_initialRouteWeightSubtitle": "Waga początkowa dla nowo odkrytych ścieżek",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maksymalna waga, jaką ścieżka może osiągnąć dzięki udanym dostarczeniom",
|
||||
"appSettings_routeWeightSuccessIncrement": "Przyrost wagi po sukcesie",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Waga dodawana do ścieżki po udanym dostarczeniu",
|
||||
"appSettings_routeWeightFailureDecrement": "Spadek wagi po niepowodzeniu",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Waga odejmowana od ścieżki po nieudanym dostarczeniu",
|
||||
"appSettings_maxMessageRetries": "Maksymalna liczba ponowień wiadomości",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponowienia przed oznaczeniem wiadomości jako nieudanej",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
"settings_allowByContact": "Zezwalaj według flag kontaktowych",
|
||||
"settings_allowAll": "Zezwalaj na wszystko",
|
||||
"settings_telemetryLocationMode": "Tryb położenia telemetrycznego",
|
||||
"settings_telemetryEnvironmentMode": "Tryb środowiska telemetrycznego",
|
||||
"settings_advertLocation": "Lokalizacja reklamowa",
|
||||
"settings_advertLocationSubtitle": "Uwzględnij lokalizację w ogłoszeniu",
|
||||
"settings_denyAll": "Odmów wszystkim",
|
||||
"settings_privacySubtitle": "Kontroluj jakie informacje są udostępniane.",
|
||||
"settings_privacy": "Ustawienia prywatności",
|
||||
"settings_privacySettingsDescription": "Wybierz jakie informacje urządzenie udostępni innym.",
|
||||
"contact_info": "Informacje kontaktowe",
|
||||
"settings_telemetryBaseMode": "Tryb podstawowy telemetrii",
|
||||
"contact_teleBase": "Baza telemetryczna",
|
||||
"contact_teleLoc": "Lokalizacja telemetryczna",
|
||||
"contact_teleLocSubtitle": "Zezwalaj na udostępnianie danych lokalizacji",
|
||||
"contact_teleEnv": "Środowisko telemetryczne",
|
||||
"contact_teleEnvSubtitle": "Zezwalaj na udostępnianie danych czujników środowiskowych",
|
||||
"contact_telemetry": "Telemetryka",
|
||||
"contact_clearChat": "Wyczyść czat",
|
||||
"contact_settings": "Ustawienia kontaktowe",
|
||||
"contact_lastSeen": "Ostatnio widziany",
|
||||
"contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Początkowa waga trasy",
|
||||
"appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu",
|
||||
"appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.",
|
||||
"appSettings_routeWeightSuccessIncrement": "Wzrost wagi sukcesu",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Waga dodana do ścieżki po pomyślnym dostarczeniu",
|
||||
"appSettings_routeWeightFailureDecrement": "Zmniejszenie wagi kary",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Waga usunięta z trasy po nieudanej dostawie",
|
||||
"appSettings_maxMessageRetries": "Maksymalna liczba prób wysłania wiadomości",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
|
||||
"settings_multiAck": "Wiele potwierdzeń: {value}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.",
|
||||
"settings_allowByContact": "Permitir por bandeiras de contato",
|
||||
"settings_telemetryLocationMode": "Modo de Localização de Telemetria",
|
||||
"settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria",
|
||||
"settings_advertLocation": "Localização do Anúncio",
|
||||
"settings_advertLocationSubtitle": "Incluir localização no anúncio",
|
||||
"settings_privacySubtitle": "Controle o que é compartilhado.",
|
||||
"settings_denyAll": "Negar todos",
|
||||
"settings_allowAll": "Permitir todos",
|
||||
"settings_privacy": "Configurações de Privacidade",
|
||||
"contact_info": "Informações de Contato",
|
||||
"settings_telemetryBaseMode": "Modo Base de Telemetria",
|
||||
"contact_teleBase": "Base de Telemetria",
|
||||
"contact_teleLoc": "Localização de Telemetria",
|
||||
"contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização",
|
||||
"contact_teleEnv": "Ambiente de Telemetria",
|
||||
"contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente",
|
||||
"contact_lastSeen": "Visto pela última vez",
|
||||
"contact_clearChat": "Limpar Chat",
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_settings": "Configurações de Contato",
|
||||
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso Inicial da Rota",
|
||||
"appSettings_maxRouteWeight": "Peso Máximo da Rota",
|
||||
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.",
|
||||
"appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
}
|
||||
|
|
@ -1140,6 +1140,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "Настройки конфиденциальности",
|
||||
"settings_privacySubtitle": "Контролируйте, какую информацию делиться.",
|
||||
"settings_telemetryLocationMode": "Режим местоположения телеметрии",
|
||||
"settings_telemetryEnvironmentMode": "Режим среды телеметрии",
|
||||
"settings_advertLocation": "Местоположение рекламы",
|
||||
"settings_advertLocationSubtitle": "Включить местоположение в объявление",
|
||||
"settings_allowAll": "Разрешить все",
|
||||
"settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.",
|
||||
"settings_denyAll": "Отклонить все",
|
||||
"settings_allowByContact": "Разрешить по флагам контактов",
|
||||
"contact_info": "Контактная информация",
|
||||
"settings_telemetryBaseMode": "Базовый режим телеметрии",
|
||||
"contact_teleBase": "База телеметрии",
|
||||
"contact_teleLoc": "Местоположение телеметрии",
|
||||
"contact_teleLocSubtitle": "Разрешить обмен данными о местоположении",
|
||||
"contact_teleEnv": "Среда телеметрии",
|
||||
"contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды",
|
||||
"contact_settings": "Настройки контактов",
|
||||
"contact_telemetry": "Телеметрия",
|
||||
"contact_clearChat": "Очистить чат",
|
||||
"contact_lastSeen": "Последний раз видели",
|
||||
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
|
||||
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
|
||||
|
|
@ -1150,5 +1179,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.",
|
||||
"appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
|
||||
"settings_multiAck": "Мульти-ACK: {value}"
|
||||
}
|
||||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "Nastavenia súkromia",
|
||||
"settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.",
|
||||
"settings_telemetryLocationMode": "Režim umiestnenia telemetrie",
|
||||
"settings_telemetryBaseMode": "Základný režim telemetrie",
|
||||
"settings_advertLocation": "Umiestnenie inzerátu",
|
||||
"settings_telemetryEnvironmentMode": "Režim prostredia telemetrie",
|
||||
"settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu",
|
||||
"settings_allowAll": "Povoliť všetko",
|
||||
"settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.",
|
||||
"settings_denyAll": "Zamietnuť všetko",
|
||||
"settings_allowByContact": "Povoliť podľa kontaktových vlajok",
|
||||
"contact_info": "Kontaktné informácie",
|
||||
"contact_settings": "Nastavenia kontaktov",
|
||||
"contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie",
|
||||
"contact_teleLoc": "Lokácia telemetrie",
|
||||
"contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite",
|
||||
"contact_teleEnv": "Prostredie telemetrie",
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_clearChat": "Vymazať chat",
|
||||
"contact_lastSeen": "Naposledy videný",
|
||||
"contact_teleBase": "Báza telemetrie",
|
||||
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
|
||||
"appSettings_initialRouteWeight": "Počiatočná váha trasy",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie",
|
||||
"appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
|
||||
"settings_multiAck": "Viaceré ACK: {value}"
|
||||
}
|
||||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "Nastavitve zasebnosti",
|
||||
"settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.",
|
||||
"settings_telemetryBaseMode": "Osnovni način telemetrije",
|
||||
"settings_telemetryLocationMode": "Način delovanja telemetrije",
|
||||
"settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije",
|
||||
"settings_advertLocation": "Lokacija oglasa",
|
||||
"settings_allowByContact": "Dovoli po kontaktnih zastavah",
|
||||
"settings_denyAll": "Zavrniti vse",
|
||||
"settings_allowAll": "Dovoli vse",
|
||||
"settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.",
|
||||
"contact_info": "Kontaktni podatki",
|
||||
"contact_teleBase": "Baza telemetrije",
|
||||
"contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije",
|
||||
"contact_teleLoc": "Lokacija telemetrije",
|
||||
"contact_lastSeen": "Zadnjič videno",
|
||||
"contact_settings": "Nastavitve stika",
|
||||
"settings_advertLocationSubtitle": "Vključi lokacijo v oglas.",
|
||||
"contact_telemetry": "Telemetrija",
|
||||
"contact_clearChat": "Počisti klepet",
|
||||
"contact_teleEnv": "Okolje telemetrije",
|
||||
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
|
||||
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
|
||||
"appSettings_initialRouteWeight": "Izvirna teža poti",
|
||||
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.",
|
||||
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "Večkratni potrditvi: {value}",
|
||||
"settings_telemetryModeUpdated": "Način telemetrije posodobljen"
|
||||
}
|
||||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacy": "Inställningar för sekretess",
|
||||
"settings_allowAll": "Tillåt alla",
|
||||
"settings_privacySubtitle": "Kontrollera vilken information som delas.",
|
||||
"settings_telemetryEnvironmentMode": "Telemetri miljöläge",
|
||||
"settings_telemetryBaseMode": "Telemetribasläge",
|
||||
"settings_telemetryLocationMode": "Telemetritillstånd för plats",
|
||||
"settings_advertLocation": "Annonsplacering",
|
||||
"contact_info": "Kontaktinformation",
|
||||
"contact_settings": "Kontaktinställningar",
|
||||
"contact_telemetry": "Telemetri",
|
||||
"settings_denyAll": "Neka alla",
|
||||
"settings_allowByContact": "Tillåt via kontaktflaggor",
|
||||
"settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.",
|
||||
"contact_lastSeen": "Senast sedd",
|
||||
"contact_clearChat": "Rensa Chatt",
|
||||
"contact_teleEnv": "Telemetri Miljö",
|
||||
"settings_advertLocationSubtitle": "Inkludera plats i annonsen",
|
||||
"contact_teleEnvSubtitle": "Tillåt delning av miljösensordata",
|
||||
"contact_teleBase": "Telemetribas",
|
||||
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
|
||||
"contact_teleLoc": "Telemetridata plats",
|
||||
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
|
||||
"appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök",
|
||||
"appSettings_maxMessageRetries": "Maximalt antal försök",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
}
|
||||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
|
||||
"settings_privacy": "Налаштування приватності",
|
||||
"settings_telemetryBaseMode": "Режим базової телеметрії",
|
||||
"settings_telemetryLocationMode": "Режим місця телеметрії",
|
||||
"settings_advertLocation": "Розміщення реклами",
|
||||
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
|
||||
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
|
||||
"settings_allowAll": "Дозволити все",
|
||||
"settings_denyAll": "Відхилити все",
|
||||
"settings_allowByContact": "Дозволити за контактними прапорцями",
|
||||
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
|
||||
"contact_info": "Контактна інформація",
|
||||
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
|
||||
"contact_teleLoc": "Розташування телеметрії",
|
||||
"contact_teleBase": "Базовий телебачення",
|
||||
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
|
||||
"contact_settings": "Налаштування контактів",
|
||||
"contact_telemetry": "Телеметрія",
|
||||
"contact_clearChat": "Очистити чат",
|
||||
"contact_lastSeen": "Останній раз бачили",
|
||||
"contact_teleEnv": "Середовище телеметрії",
|
||||
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Початкова вартість маршруту",
|
||||
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
|
||||
"appSettings_maxRouteWeight": "Максимальна вага маршруту",
|
||||
|
|
@ -1910,5 +1939,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки",
|
||||
"appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
|
||||
"settings_multiAck": "Багатократне підтвердження: {value}"
|
||||
}
|
||||
|
|
@ -1905,6 +1905,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings_privacySubtitle": "控制要共享的信息。",
|
||||
"settings_privacySettingsDescription": "选择您的设备与他人共享的信息。",
|
||||
"settings_telemetryBaseMode": "遥测基础模式",
|
||||
"settings_telemetryLocationMode": "遥测位置模式",
|
||||
"settings_advertLocation": "广告位置",
|
||||
"settings_advertLocationSubtitle": "在广告中包含位置",
|
||||
"settings_allowByContact": "按联系人标志允许",
|
||||
"settings_denyAll": "拒绝所有",
|
||||
"settings_privacy": "隐私设置",
|
||||
"settings_allowAll": "允许全部",
|
||||
"contact_info": "联系信息",
|
||||
"contact_teleBase": "遥测基站",
|
||||
"contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据",
|
||||
"settings_telemetryEnvironmentMode": "遥测环境模式",
|
||||
"contact_teleLoc": "遥测位置",
|
||||
"contact_teleEnv": "遥测环境",
|
||||
"contact_teleEnvSubtitle": "允许共享环境传感器数据",
|
||||
"contact_clearChat": "清除聊天记录",
|
||||
"contact_lastSeen": "最近出现",
|
||||
"contact_settings": "联系人设置",
|
||||
"contact_teleLocSubtitle": "允许共享位置数据",
|
||||
"contact_telemetry": "遥测数据",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "最大路径重量",
|
||||
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
|
||||
"appSettings_initialRouteWeight": "初始路线权重",
|
||||
|
|
@ -1915,5 +1944,7 @@
|
|||
"appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。",
|
||||
"appSettings_maxMessageRetries": "最大消息重试次数",
|
||||
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
|
||||
"path_routeWeight": "{weight}/{max}"
|
||||
}
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"settings_multiAck": "多重ACK:{value}",
|
||||
"settings_telemetryModeUpdated": "遥测模式已更新"
|
||||
}
|
||||
|
|
@ -24,20 +24,23 @@ class Channel {
|
|||
|
||||
bool get isPublicChannel => pskHex == publicChannelPsk;
|
||||
|
||||
static Channel? fromFrame(Uint8List data) {
|
||||
static Channel? fromFrame(Uint8List frame) {
|
||||
// CHANNEL_INFO format:
|
||||
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
||||
// [1] = channel_idx
|
||||
// [2-33] = name (32 bytes, null-terminated)
|
||||
// [34-49] = psk (16 bytes)
|
||||
if (data.length < 50) return null;
|
||||
if (data[0] != respCodeChannelInfo) return null;
|
||||
|
||||
final index = data[1];
|
||||
final name = readCString(data, 2, 32);
|
||||
final psk = Uint8List.fromList(data.sublist(34, 50));
|
||||
|
||||
return Channel(index: index, name: name, psk: psk);
|
||||
if (frame.length < 50) return null;
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
if (reader.readByte() != respCodeChannelInfo) return null;
|
||||
final index = reader.readByte();
|
||||
final name = reader.readCStringGreedy(32);
|
||||
final psk = reader.readBytes(16);
|
||||
return Channel(index: index, name: name, psk: psk);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Channel empty(int index) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
enum ChannelMessageStatus { pending, sent, failed }
|
||||
|
||||
|
|
@ -109,89 +110,82 @@ class ChannelMessage {
|
|||
);
|
||||
}
|
||||
|
||||
static ChannelMessage? fromFrame(Uint8List data) {
|
||||
static ChannelMessage? fromFrame(Uint8List frame) {
|
||||
// CHANNEL_MSG_RECV format varies by version:
|
||||
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
|
||||
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
|
||||
if (data.length < 8) return null;
|
||||
if (frame.length < 8) return null;
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
final code = reader.readByte();
|
||||
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final code = data[0];
|
||||
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
||||
int pathLen;
|
||||
int txtType;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
int channelIdx;
|
||||
if (code == respCodeChannelMsgRecvV3) {
|
||||
reader.skipBytes(1); // Skip SNR
|
||||
final flags = reader.readByte();
|
||||
final hasPath = (flags & 0x01) != 0;
|
||||
reader.skipBytes(1); // Skip reserved byte
|
||||
channelIdx = reader.readByte();
|
||||
pathLen = reader.readInt8();
|
||||
txtType = reader.readByte();
|
||||
if (hasPath && pathLen > 0) {
|
||||
reader.rewind(); // Rewind to read path length again for pathBytes
|
||||
pathBytes = reader.readBytes(pathLen);
|
||||
}
|
||||
} else {
|
||||
channelIdx = reader.readByte();
|
||||
pathLen = reader.readInt8();
|
||||
txtType = reader.readByte();
|
||||
}
|
||||
final timestampRaw = reader.readUInt32LE();
|
||||
|
||||
if (txtType != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final text = reader.readCString();
|
||||
|
||||
// Extract sender name and actual message from "name: msg" format
|
||||
String senderName = 'Unknown';
|
||||
String actualText = text;
|
||||
|
||||
final colonIndex = text.indexOf(':');
|
||||
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
|
||||
final potentialSender = text.substring(0, colonIndex);
|
||||
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||
senderName = potentialSender;
|
||||
final offset =
|
||||
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
? colonIndex + 2
|
||||
: colonIndex + 1;
|
||||
actualText = text.substring(offset);
|
||||
}
|
||||
}
|
||||
|
||||
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
|
||||
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: pathLen,
|
||||
pathBytes: pathBytes,
|
||||
channelIndex: channelIdx,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.error('Error parsing channel message frame: $e');
|
||||
// If parsing fails, return null to avoid crashes
|
||||
return null;
|
||||
}
|
||||
|
||||
int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
int channelIdx;
|
||||
|
||||
if (code == respCodeChannelMsgRecvV3) {
|
||||
channelIdx = data[4];
|
||||
pathLenOffset = 5;
|
||||
final pathLen = data[pathLenOffset].toSigned(8);
|
||||
var cursor = 6;
|
||||
final hasPathBytesFlag = (data[2] & 0x01) != 0;
|
||||
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
|
||||
final hasValidTxtType =
|
||||
cursor < data.length &&
|
||||
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
|
||||
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
|
||||
canFitPath) {
|
||||
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
|
||||
cursor += pathLen;
|
||||
}
|
||||
txtTypeOffset = cursor;
|
||||
cursor += 1; // txt_type
|
||||
timestampOffset = cursor;
|
||||
textOffset = cursor + 4;
|
||||
} else {
|
||||
channelIdx = data[1];
|
||||
pathLenOffset = 2;
|
||||
txtTypeOffset = 3;
|
||||
timestampOffset = 4;
|
||||
textOffset = 8;
|
||||
}
|
||||
|
||||
if (data.length < textOffset + 1) return null;
|
||||
|
||||
final txtType = data[txtTypeOffset];
|
||||
if (txtType != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final pathLen = data[pathLenOffset].toSigned(8);
|
||||
final timestampRaw = readUint32LE(data, timestampOffset);
|
||||
final text = readCString(data, textOffset, data.length - textOffset);
|
||||
|
||||
// Extract sender name and actual message from "name: msg" format
|
||||
String senderName = 'Unknown';
|
||||
String actualText = text;
|
||||
|
||||
final colonIndex = text.indexOf(':');
|
||||
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
|
||||
final potentialSender = text.substring(0, colonIndex);
|
||||
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||
senderName = potentialSender;
|
||||
final offset =
|
||||
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
? colonIndex + 2
|
||||
: colonIndex + 1;
|
||||
actualText = text.substring(offset);
|
||||
}
|
||||
}
|
||||
|
||||
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
|
||||
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: pathLen,
|
||||
pathBytes: pathBytes,
|
||||
channelIndex: channelIdx,
|
||||
);
|
||||
}
|
||||
|
||||
static ChannelMessage outgoing(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class Contact {
|
|||
final DateTime lastSeen;
|
||||
final DateTime lastMessageAt;
|
||||
final bool isActive;
|
||||
final bool wasPulled;
|
||||
final Uint8List? rawPacket;
|
||||
|
||||
Contact({
|
||||
|
|
@ -34,6 +35,7 @@ class Contact {
|
|||
required this.lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
this.isActive = true,
|
||||
this.wasPulled = false,
|
||||
this.rawPacket,
|
||||
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
||||
|
||||
|
|
@ -214,4 +216,7 @@ class Contact {
|
|||
|
||||
@override
|
||||
int get hashCode => publicKeyHex.hashCode;
|
||||
bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
|
||||
bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
|
||||
bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class Message {
|
|||
final String? messageId;
|
||||
final int retryCount;
|
||||
final int? estimatedTimeoutMs;
|
||||
final Uint8List? expectedAckHash;
|
||||
final int? expectedAckHash;
|
||||
final DateTime? sentAt;
|
||||
final DateTime? deliveredAt;
|
||||
final int? tripTimeMs;
|
||||
|
|
@ -56,7 +56,7 @@ class Message {
|
|||
MessageStatus? status,
|
||||
int? retryCount,
|
||||
int? estimatedTimeoutMs,
|
||||
Uint8List? expectedAckHash,
|
||||
int? expectedAckHash,
|
||||
DateTime? sentAt,
|
||||
DateTime? deliveredAt,
|
||||
int? tripTimeMs,
|
||||
|
|
@ -90,33 +90,35 @@ class Message {
|
|||
);
|
||||
}
|
||||
|
||||
static Message? fromFrame(Uint8List data, Uint8List selfPubKey) {
|
||||
if (data.length < msgTextOffset + 1) return null;
|
||||
static Message? fromFrame(Uint8List frame, Uint8List selfPubKey) {
|
||||
if (frame.length < msgTextOffset + 1) return null;
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
final code = reader.readByte();
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final code = data[0];
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
final senderKey = reader.readBytes(pubKeySize);
|
||||
final timestampRaw = reader.readInt32LE();
|
||||
final flags = reader.readByte();
|
||||
if ((flags >> 2) != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
final text = reader.readCString();
|
||||
|
||||
return Message(
|
||||
senderKey: senderKey,
|
||||
text: text,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
isCli: false,
|
||||
status: MessageStatus.delivered,
|
||||
pathBytes: Uint8List(0),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final senderKey = Uint8List.fromList(
|
||||
data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final timestampRaw = readUint32LE(data, msgTimestampOffset);
|
||||
final flags = data[msgFlagsOffset];
|
||||
if ((flags >> 2) != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
|
||||
|
||||
return Message(
|
||||
senderKey: senderKey,
|
||||
text: text,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
isCli: false,
|
||||
status: MessageStatus.delivered,
|
||||
pathBytes: Uint8List(0),
|
||||
);
|
||||
}
|
||||
|
||||
static Message outgoing(
|
||||
|
|
|
|||
|
|
@ -283,66 +283,66 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||
if (payload.length < 101) {
|
||||
return 'ADVERT (short)';
|
||||
}
|
||||
var offset = 0;
|
||||
final pubKey = _bytesToHex(
|
||||
payload.sublist(offset, offset + 32),
|
||||
spaced: false,
|
||||
);
|
||||
offset += 32;
|
||||
final timestamp = readUint32LE(payload, offset);
|
||||
offset += 4;
|
||||
offset += 64; // signature
|
||||
final flags = payload[offset++];
|
||||
final role = _deviceRoleLabel(flags & 0x0F);
|
||||
final hasLocation = (flags & 0x10) != 0;
|
||||
final hasFeature1 = (flags & 0x20) != 0;
|
||||
final hasFeature2 = (flags & 0x40) != 0;
|
||||
final hasName = (flags & 0x80) != 0;
|
||||
String? name;
|
||||
double? lat;
|
||||
double? lon;
|
||||
if (hasLocation && payload.length >= offset + 8) {
|
||||
lat = readInt32LE(payload, offset) / 1000000.0;
|
||||
lon = readInt32LE(payload, offset + 4) / 1000000.0;
|
||||
offset += 8;
|
||||
final reader = BufferReader(payload);
|
||||
try {
|
||||
final pubKey = _bytesToHex(reader.readBytes(pubKeySize), spaced: false);
|
||||
|
||||
final timestamp = reader.readUInt32LE();
|
||||
reader.skipBytes(signatureSize);
|
||||
final flags = reader.readByte();
|
||||
final role = _deviceRoleLabel(flags & 0x0F);
|
||||
final hasLocation = (flags & 0x10) != 0;
|
||||
final hasFeature1 = (flags & 0x20) != 0;
|
||||
final hasFeature2 = (flags & 0x40) != 0;
|
||||
final hasName = (flags & 0x80) != 0;
|
||||
String? name;
|
||||
double? lat;
|
||||
double? lon;
|
||||
if (hasLocation) {
|
||||
lat = reader.readInt32LE() / 1000000.0;
|
||||
lon = reader.readInt32LE() / 1000000.0;
|
||||
}
|
||||
if (hasFeature1) reader.skipBytes(2);
|
||||
if (hasFeature2) reader.skipBytes(2);
|
||||
if (hasName) {
|
||||
name = reader.readCStringGreedy(maxNameSize);
|
||||
}
|
||||
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
|
||||
final locPart = (lat != null && lon != null)
|
||||
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
||||
: '';
|
||||
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
||||
} catch (e) {
|
||||
return 'ADVERT (invalid)';
|
||||
}
|
||||
if (hasFeature1) offset += 2;
|
||||
if (hasFeature2) offset += 2;
|
||||
if (hasName && payload.length > offset) {
|
||||
final rawName = String.fromCharCodes(payload.sublist(offset));
|
||||
final nul = rawName.indexOf('\u0000');
|
||||
name = nul >= 0 ? rawName.substring(0, nul) : rawName;
|
||||
name = name.trim();
|
||||
}
|
||||
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
|
||||
final locPart = (lat != null && lon != null)
|
||||
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
||||
: '';
|
||||
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
||||
}
|
||||
|
||||
String _decodeControlSummary(Uint8List payload) {
|
||||
if (payload.isEmpty) return 'CONTROL (empty)';
|
||||
final flags = payload[0];
|
||||
final subType = flags & 0xF0;
|
||||
if (subType == 0x80) {
|
||||
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
||||
final typeFilter = payload[1];
|
||||
final tag = readUint32LE(payload, 2);
|
||||
final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0;
|
||||
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
||||
final reader = BufferReader(payload);
|
||||
try {
|
||||
final flags = reader.readByte();
|
||||
final subType = flags & 0xF0;
|
||||
if (subType == 0x80) {
|
||||
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
||||
final typeFilter = reader.readByte();
|
||||
final tag = reader.readInt32LE();
|
||||
final since = payload.length >= 10 ? reader.readInt32LE() : 0;
|
||||
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
||||
}
|
||||
if (subType == 0x90) {
|
||||
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
|
||||
final nodeType = flags & 0x0F;
|
||||
final snrRaw = payload[1];
|
||||
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
||||
final snr = snrSigned / 4.0;
|
||||
final tag = reader.readInt32LE();
|
||||
final keyLen = payload.length - 6;
|
||||
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
|
||||
}
|
||||
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
|
||||
} catch (e) {
|
||||
return 'CONTROL (invalid)';
|
||||
}
|
||||
if (subType == 0x90) {
|
||||
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
|
||||
final nodeType = flags & 0x0F;
|
||||
final snrRaw = payload[1];
|
||||
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
||||
final snr = snrSigned / 4.0;
|
||||
final tag = readUint32LE(payload, 2);
|
||||
final keyLen = payload.length - 6;
|
||||
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
|
||||
}
|
||||
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _payloadTypeLabel(int payloadType) {
|
||||
|
|
|
|||
|
|
@ -166,6 +166,33 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
if (value == 'clearChat') {
|
||||
context.read<MeshCoreConnector>().clearMessagesForChannel(
|
||||
widget.channel.index,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'clearChat',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.contact_clearChat,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import '../widgets/gif_picker.dart';
|
|||
import '../widgets/path_selection_dialog.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
|
|
@ -246,9 +247,77 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
tooltip: context.l10n.chat_pathManagement,
|
||||
onPressed: () => _showPathHistory(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () => _showContactInfo(context),
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
if (value == 'info') {
|
||||
_showContactInfo(context);
|
||||
}
|
||||
if (value == 'settings') {
|
||||
_showContactSettings(context);
|
||||
}
|
||||
if (value == 'telemetry') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TelemetryScreen(contact: widget.contact),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (value == 'clearChat') {
|
||||
connector.clearMessagesForContact(widget.contact);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'info',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.contact_info),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'telemetry',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.bar_chart, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.contact_telemetry),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'settings',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.contact_settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'clearChat',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.contact_clearChat,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -895,11 +964,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
int _resolveContactIndex = -1;
|
||||
|
||||
Contact _resolveContact(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
if (_resolveContactIndex >= 0 &&
|
||||
_resolveContactIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveContactIndex].publicKeyHex ==
|
||||
widget.contact.publicKeyHex) {
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
_resolveContactIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||
orElse: () => widget.contact,
|
||||
);
|
||||
if (_resolveContactIndex == -1) {
|
||||
return widget.contact;
|
||||
}
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
Contact _resolveContactFrom4Bytes(
|
||||
|
|
@ -952,59 +1032,127 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
|
||||
void _showContactInfo(BuildContext context) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
|
||||
|
||||
final contact = _resolveContact(connector);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final contact = _resolveContact(connector);
|
||||
final smazEnabled = connector.isContactSmazEnabled(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(contact.name),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_location,
|
||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_publicKey,
|
||||
'${contact.publicKeyHex.substring(0, 16)}...',
|
||||
),
|
||||
const Divider(),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.channels_smazCompression),
|
||||
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
|
||||
value: smazEnabled,
|
||||
onChanged: (value) {
|
||||
connector.setContactSmazEnabled(
|
||||
contact.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
builder: (context) => AlertDialog(
|
||||
title: SelectableText(contact.name),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
_formatContactLastMessage(contact.lastMessageAt),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_location,
|
||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||
),
|
||||
_buildInfoRow(context.l10n.chat_publicKey, contact.publicKeyHex),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContactSettings(BuildContext context) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
|
||||
final contact = widget.contact;
|
||||
bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
|
||||
bool teleBaseEnabled = contact.teleBaseEnabled;
|
||||
bool teleLocEnabled = contact.teleLocEnabled;
|
||||
bool teleEnvEnabled = contact.teleEnvEnabled;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(context.l10n.contact_settings),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (contact.hasLocation) ...[
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_location,
|
||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||
),
|
||||
const Divider(height: 8),
|
||||
],
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.channels_smazCompression),
|
||||
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
|
||||
value: smazEnabled,
|
||||
onChanged: (value) {
|
||||
connector.setContactSmazEnabled(
|
||||
contact.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
setDialogState(() => smazEnabled = value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.contact_teleBase),
|
||||
subtitle: Text(context.l10n.contact_teleBaseSubtitle),
|
||||
value: teleBaseEnabled,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => teleBaseEnabled = value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.contact_teleLoc),
|
||||
subtitle: Text(context.l10n.contact_teleLocSubtitle),
|
||||
value: teleLocEnabled,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => teleLocEnabled = value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.contact_teleEnv),
|
||||
subtitle: Text(context.l10n.contact_teleEnvSubtitle),
|
||||
value: teleEnvEnabled,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => teleEnvEnabled = value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
connector.setContactFlags(
|
||||
contact,
|
||||
teleBase: teleBaseEnabled,
|
||||
teleLoc: teleLocEnabled,
|
||||
teleEnv: teleEnvEnabled,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -1019,12 +1167,32 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
width: 80,
|
||||
child: Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
Expanded(child: SelectableText(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatContactLastMessage(DateTime timestamp) {
|
||||
final diff = DateTime.now().difference(timestamp);
|
||||
if (diff.isNegative || diff.inMinutes < 5) {
|
||||
return context.l10n.contacts_lastSeenNow;
|
||||
}
|
||||
if (diff.inMinutes < 60) {
|
||||
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||
}
|
||||
if (diff.inHours < 24) {
|
||||
final hours = diff.inHours;
|
||||
return hours == 1
|
||||
? context.l10n.contacts_lastSeenHourAgo
|
||||
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
||||
}
|
||||
final days = diff.inDays;
|
||||
return days == 1
|
||||
? context.l10n.contacts_lastSeenDayAgo
|
||||
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||
}
|
||||
|
||||
void _openChat(BuildContext context, Contact contact) {
|
||||
// Check if this is a repeater
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
|
|
|
|||
|
|
@ -1354,7 +1354,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await connector.setContactFavorite(contact, !isFavorite);
|
||||
await connector.setContactFlags(
|
||||
contact,
|
||||
isFavorite: !isFavorite,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,24 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||
PathSelection? _pendingStatusSelection;
|
||||
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -163,13 +181,6 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadNeighbors() async {
|
||||
if (_commandService == null) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -77,11 +77,22 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
void _handleTextMessageResponse(Uint8List frame) {
|
||||
|
|
|
|||
|
|
@ -205,8 +205,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TelemetryScreen(repeater: repeater, password: password),
|
||||
builder: (context) => TelemetryScreen(contact: repeater),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -129,11 +129,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||
_commandService?.handleResponse(widget.repeater, parsed.text);
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
bool _matchesRepeaterPrefix(Uint8List prefix) {
|
||||
|
|
|
|||
|
|
@ -91,11 +91,22 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
void _handleTextMessageResponse(Uint8List frame) {
|
||||
|
|
|
|||
|
|
@ -287,10 +287,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.visibility_off_outlined),
|
||||
title: Text(l10n.settings_privacyMode),
|
||||
subtitle: Text(l10n.settings_privacyModeSubtitle),
|
||||
title: Text(l10n.settings_privacy),
|
||||
subtitle: Text(l10n.settings_privacySubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _togglePrivacy(context, connector),
|
||||
onTap: () => _privacySettings(context, connector),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -657,47 +657,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
void _togglePrivacy(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.settings_privacyMode),
|
||||
content: Text(l10n.settings_privacyModeToggle),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setPrivacyMode(true);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_privacyModeEnabled)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_enable),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setPrivacyMode(false);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_privacyModeDisabled)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_disable),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
connector.sendSelfAdvert(flood: true);
|
||||
|
|
@ -977,6 +936,136 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
int telemetryMode = connector.telemetryModeBase;
|
||||
int telemetryLocMode = connector.telemetryModeLoc;
|
||||
int telemetryEnvMode = connector.telemetryModeEnv;
|
||||
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
|
||||
int multiAcks = connector.multiAcks;
|
||||
|
||||
final telemModeBase = [
|
||||
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
|
||||
DropdownMenuItem(
|
||||
value: teleModeAllowFlags,
|
||||
child: Text(l10n.settings_allowByContact),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: teleModeAllowAll,
|
||||
child: Text(l10n.settings_allowAll),
|
||||
),
|
||||
];
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.settings_privacy),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.settings_privacySettingsDescription),
|
||||
const SizedBox(height: 16),
|
||||
FeatureToggleRow(
|
||||
title: l10n.settings_advertLocation,
|
||||
subtitle: l10n.settings_advertLocationSubtitle,
|
||||
value: advertLocPolicy,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => advertLocPolicy = value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: telemetryMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_telemetryBaseMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: telemModeBase,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setDialogState(() => telemetryMode = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: telemetryLocMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_telemetryLocationMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: telemModeBase,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setDialogState(() => telemetryLocMode = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: telemetryEnvMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_telemetryEnvironmentMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: telemModeBase,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setDialogState(() => telemetryEnvMode = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.settings_multiAck(multiAcks.toString()),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Slider(
|
||||
value: multiAcks.toDouble(),
|
||||
min: 0,
|
||||
max: 2,
|
||||
divisions: 2,
|
||||
label: multiAcks.toString(),
|
||||
onChanged: (value) {
|
||||
setDialogState(() => multiAcks = value.round());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setTelemetryModeBase(
|
||||
telemetryMode,
|
||||
telemetryLocMode,
|
||||
telemetryEnvMode,
|
||||
advertLocPolicy ? 1 : 0,
|
||||
multiAcks,
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _RadioSettingsDialog extends StatefulWidget {
|
||||
final MeshCoreConnector connector;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,30 +10,22 @@ import '../connector/meshcore_connector.dart';
|
|||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
final Contact contact;
|
||||
|
||||
const TelemetryScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
});
|
||||
const TelemetryScreen({super.key, required this.contact});
|
||||
|
||||
@override
|
||||
State<TelemetryScreen> createState() => _TelemetryScreenState();
|
||||
}
|
||||
|
||||
class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
static const int _statusPayloadOffset = 8;
|
||||
static const int _statusStatsSize = 52;
|
||||
static const int _statusResponseBytes =
|
||||
_statusPayloadOffset + _statusStatsSize;
|
||||
Uint8List _tagData = Uint8List(4);
|
||||
int _tagData = 0;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isLoaded = false;
|
||||
|
|
@ -44,6 +36,26 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
PathSelection? _pendingStatusSelection;
|
||||
List<Map<String, dynamic>>? _parsedTelemetry;
|
||||
|
||||
int _tripTime = 0;
|
||||
|
||||
int _resolveContactIndex = -1;
|
||||
|
||||
Contact _resolveContact(MeshCoreConnector connector) {
|
||||
if (_resolveContactIndex >= 0 &&
|
||||
_resolveContactIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveContactIndex].publicKeyHex ==
|
||||
widget.contact.publicKeyHex) {
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
_resolveContactIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||
);
|
||||
if (_resolveContactIndex == -1) {
|
||||
return widget.contact;
|
||||
}
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -60,27 +72,62 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
// Listen for incoming text messages from the repeater
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
final cmd = reader.readByte();
|
||||
if (cmd == respCodeSent) {
|
||||
reader.skipBytes(1); // Skip the reserved byte
|
||||
_tagData = reader.readUInt32LE();
|
||||
_tripTime = reader.readUInt32LE();
|
||||
_statusTimeout?.cancel();
|
||||
_statusTimeout = Timer(Duration(milliseconds: _tripTime), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
_recordTelemetryResult(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (frame[0] == respCodeSent) {
|
||||
_tagData = frame.sublist(2, 6);
|
||||
}
|
||||
// Check if it's a binary response
|
||||
if (cmd == pushCodeBinaryResponse) {
|
||||
if (!mounted) return;
|
||||
reader.skipBytes(1); // Skip the reserved byte
|
||||
if (reader.readUInt32LE() != _tagData) return;
|
||||
_handleTelemetryResponse(reader.readRemainingBytes());
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (frame[0] == pushCodeBinaryResponse &&
|
||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||
if (!mounted) return;
|
||||
_handleStatusResponse(frame.sublist(6));
|
||||
// Check if it's a telemetry response (for chat contacts)
|
||||
if (cmd == pushCodeTelemetryResponse) {
|
||||
reader.skipBytes(1); // Skip the reserved byte
|
||||
final pubkey = reader.readBytes(6);
|
||||
if (!mounted) return;
|
||||
if (!listEquals(widget.contact.publicKey.sublist(0, 6), pubkey)) {
|
||||
return;
|
||||
}
|
||||
_handleTelemetryResponse(reader.readRemainingBytes());
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.error('Error parsing incoming frame: $e');
|
||||
// If parsing fails, ignore the frame
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleStatusResponse(Uint8List frame) {
|
||||
void _handleTelemetryResponse(Uint8List frame) {
|
||||
final parsedTelemetry = CayenneLpp.parseByChannel(frame);
|
||||
final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
|
||||
if (batteryMv != null) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.updateRepeaterBatterySnapshot(
|
||||
widget.repeater.publicKeyHex,
|
||||
widget.contact.publicKeyHex,
|
||||
batteryMv,
|
||||
source: 'telemetry',
|
||||
);
|
||||
|
|
@ -105,13 +152,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadTelemetry() async {
|
||||
if (_commandService == null) return;
|
||||
|
||||
|
|
@ -121,41 +161,20 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
});
|
||||
try {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final selection = await connector.preparePathForContactSend(repeater);
|
||||
final selection = await connector.preparePathForContactSend(
|
||||
_resolveContact(connector),
|
||||
);
|
||||
_pendingStatusSelection = selection;
|
||||
final frame = buildSendBinaryReq(
|
||||
repeater.publicKey,
|
||||
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
|
||||
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
|
||||
var messageBytes = frame.length >= _statusResponseBytes
|
||||
? frame.length
|
||||
: _statusResponseBytes;
|
||||
if (messageBytes < maxFrameSize) {
|
||||
messageBytes = maxFrameSize;
|
||||
}
|
||||
final timeoutMs = connector.calculateTimeout(
|
||||
pathLength: pathLengthValue,
|
||||
messageBytes: messageBytes,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
Uint8List frame;
|
||||
if (widget.contact.type != advTypeChat) {
|
||||
frame = buildSendBinaryReq(
|
||||
widget.contact.publicKey,
|
||||
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
} else {
|
||||
frame = buildSendTelemetryReq(widget.contact.publicKey);
|
||||
}
|
||||
await connector.sendFrame(frame);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
|
@ -173,12 +192,16 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
void _recordStatusResult(bool success) {
|
||||
void _recordTelemetryResult(bool success) {
|
||||
final selection = _pendingStatusSelection;
|
||||
if (selection == null) return;
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
connector.recordRepeaterPathResult(repeater, selection, success, null);
|
||||
connector.recordRepeaterPathResult(
|
||||
widget.contact,
|
||||
selection,
|
||||
success,
|
||||
null,
|
||||
);
|
||||
_pendingStatusSelection = null;
|
||||
}
|
||||
|
||||
|
|
@ -196,8 +219,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
final connector = context.watch<MeshCoreConnector>();
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final isFloodMode = repeater.pathOverride == -1;
|
||||
final isFloodMode = widget.contact.pathOverride == -1;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -210,7 +232,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
repeater.name,
|
||||
widget.contact.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
|
|
@ -225,9 +247,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
await connector.setPathOverride(widget.contact, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
await connector.setPathOverride(widget.contact, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
|
|
@ -283,7 +305,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
PathManagementDialog.show(context, contact: widget.contact),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
|
|
@ -437,7 +459,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
final l10n = context.l10n;
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final batteryMv =
|
||||
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||
connector.getRepeaterBatteryMillivolts(widget.contact.publicKeyHex) ??
|
||||
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
|
||||
if (batteryMv == null) return l10n.common_notAvailable;
|
||||
final chemistry = _batteryChemistry();
|
||||
|
|
@ -449,7 +471,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
String _batteryChemistry() {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
return settingsService.batteryChemistryForRepeater(
|
||||
widget.repeater.publicKeyHex,
|
||||
widget.contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'app_debug_log_service.dart';
|
|||
|
||||
class _AckHistoryEntry {
|
||||
final String messageId;
|
||||
final List<Uint8List> ackHashes;
|
||||
final List<int> ackHashes;
|
||||
final DateTime timestamp;
|
||||
|
||||
_AckHistoryEntry({
|
||||
|
|
@ -77,7 +77,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
final Map<String, Contact> _pendingContacts = {};
|
||||
final Map<String, List<PathSelection>> _attemptPathHistory = {};
|
||||
final Map<String, AckHashMapping> _ackHashToMessageId = {};
|
||||
final Map<String, List<Uint8List>> _expectedAckHashes = {};
|
||||
final Map<String, List<int>> _expectedAckHashes = {};
|
||||
final List<_AckHistoryEntry> _ackHistory = [];
|
||||
final Map<String, List<String>> _sendQueue = {};
|
||||
final Set<String> _activeMessages = {};
|
||||
|
|
@ -98,7 +98,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
|
||||
/// Compute expected ACK hash using same algorithm as firmware:
|
||||
/// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes
|
||||
static Uint8List computeExpectedAckHash(
|
||||
static int computeExpectedAckHash(
|
||||
int timestampSeconds,
|
||||
int attempt,
|
||||
String text,
|
||||
|
|
@ -126,7 +126,8 @@ class MessageRetryService extends ChangeNotifier {
|
|||
|
||||
// Compute SHA256 and return first 4 bytes
|
||||
final hash = sha256.convert(buffer);
|
||||
return Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
|
||||
}
|
||||
|
||||
Future<void> sendMessageWithRetry({
|
||||
|
|
@ -324,9 +325,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
outboundText,
|
||||
selfPubKey,
|
||||
);
|
||||
final expectedHashHex = expectedHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final expectedHashHex = expectedHash.toRadixString(16).padLeft(8, '0');
|
||||
_expectedHashToMessageId[expectedHashHex] = messageId;
|
||||
|
||||
final shortText = message.text.length > 20
|
||||
|
|
@ -341,13 +340,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||
config.sendMessage(contact, message.text, attempt, timestampSeconds);
|
||||
}
|
||||
|
||||
bool updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
||||
bool updateMessageFromSent(int ackHash, int timeoutMs) {
|
||||
final config = _config;
|
||||
if (config == null) return false;
|
||||
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||
|
||||
// Try hash-based matching (fixes LoRa message drops causing mismatches)
|
||||
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
|
||||
|
|
@ -389,10 +386,8 @@ class MessageRetryService extends ChangeNotifier {
|
|||
|
||||
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
||||
_expectedAckHashes[messageId] ??= [];
|
||||
if (!_expectedAckHashes[messageId]!.any(
|
||||
(hash) => listEquals(hash, ackHash),
|
||||
)) {
|
||||
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
|
||||
if (!_expectedAckHashes[messageId]!.any((hash) => hash == ackHash)) {
|
||||
_expectedAckHashes[messageId]!.add(ackHash);
|
||||
}
|
||||
|
||||
// Calculate timeout: prefer ML prediction, then device-provided, then physics fallback
|
||||
|
|
@ -559,10 +554,10 @@ class MessageRetryService extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
bool _checkAckHistory(Uint8List ackHash) {
|
||||
bool _checkAckHistory(int ackHash) {
|
||||
for (final entry in _ackHistory) {
|
||||
for (final expectedHash in entry.ackHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
if (expectedHash == ackHash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -570,13 +565,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||
return false;
|
||||
}
|
||||
|
||||
void handleAckReceived(Uint8List ackHash, int tripTimeMs) {
|
||||
void handleAckReceived(int ackHash, int tripTimeMs) {
|
||||
final config = _config;
|
||||
String? matchedMessageId;
|
||||
int? matchedAttemptIndex;
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||
|
||||
// Clean up old ACK hash mappings (older than 15 minutes)
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15));
|
||||
|
|
@ -606,7 +599,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
final expectedHashes = entry.value;
|
||||
|
||||
for (final expectedHash in expectedHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
if (expectedHash == ackHash) {
|
||||
matchedMessageId = messageId;
|
||||
matchedAttemptIndex = expectedHashes.indexOf(expectedHash);
|
||||
break;
|
||||
|
|
@ -685,11 +678,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
String? getContactKeyForAckHash(Uint8List ackHash) {
|
||||
String? getContactKeyForAckHash(int ackHash) {
|
||||
for (var entry in _pendingMessages.entries) {
|
||||
final message = entry.value;
|
||||
if (message.expectedAckHash != null &&
|
||||
listEquals(message.expectedAckHash, ackHash)) {
|
||||
message.expectedAckHash == ackHash) {
|
||||
final contact = _pendingContacts[entry.key];
|
||||
return contact?.publicKeyHex;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,9 +85,7 @@ class MessageStore {
|
|||
'messageId': msg.messageId,
|
||||
'retryCount': msg.retryCount,
|
||||
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
|
||||
'expectedAckHash': msg.expectedAckHash != null
|
||||
? base64Encode(msg.expectedAckHash!)
|
||||
: null,
|
||||
'expectedAckHash': msg.expectedAckHash,
|
||||
'sentAt': msg.sentAt?.millisecondsSinceEpoch,
|
||||
'deliveredAt': msg.deliveredAt?.millisecondsSinceEpoch,
|
||||
'tripTimeMs': msg.tripTimeMs,
|
||||
|
|
@ -119,9 +117,7 @@ class MessageStore {
|
|||
messageId: json['messageId'] as String?,
|
||||
retryCount: json['retryCount'] as int? ?? 0,
|
||||
estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?,
|
||||
expectedAckHash: json['expectedAckHash'] != null
|
||||
? Uint8List.fromList(base64Decode(json['expectedAckHash'] as String))
|
||||
: null,
|
||||
expectedAckHash: json['expectedAckHash'] as int? ?? 0,
|
||||
sentAt: json['sentAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(json['sentAt'] as int)
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -34,11 +34,22 @@ class _PathManagementDialog extends StatefulWidget {
|
|||
class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
bool _showAllPaths = false;
|
||||
|
||||
int _resolveContactIndex = -1;
|
||||
|
||||
Contact _resolveContact(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
if (_resolveContactIndex >= 0 &&
|
||||
_resolveContactIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveContactIndex].publicKeyHex ==
|
||||
widget.contact.publicKeyHex) {
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
_resolveContactIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||
orElse: () => widget.contact,
|
||||
);
|
||||
if (_resolveContactIndex == -1) {
|
||||
return widget.contact;
|
||||
}
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
String _formatRelativeTime(BuildContext context, DateTime? time) {
|
||||
|
|
|
|||
|
|
@ -69,11 +69,21 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
|||
|
||||
bool _isLoggingIn = false;
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
|
|
|
|||
|
|
@ -64,11 +64,22 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
|||
|
||||
bool _isLoggingIn = false;
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.room.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.room.publicKeyHex,
|
||||
orElse: () => widget.room,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.room;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import 'package:meshcore_open/services/message_retry_service.dart';
|
|||
|
||||
/// Replicates the SHA-256 computation from [MessageRetryService.computeExpectedAckHash]
|
||||
/// so tests can cross-check without calling the real implementation twice.
|
||||
Uint8List _manualAckHash(
|
||||
int _manualAckHash(
|
||||
int timestampSeconds,
|
||||
int attemptMasked, // already masked to 0x03
|
||||
String text,
|
||||
|
|
@ -35,7 +35,8 @@ Uint8List _manualAckHash(
|
|||
buffer.setRange(offset, offset + senderPubKey.length, senderPubKey);
|
||||
|
||||
final hash = sha256.convert(buffer);
|
||||
return Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
|
||||
}
|
||||
|
||||
Uint8List _makeKey(int seed) {
|
||||
|
|
@ -169,16 +170,6 @@ void main() {
|
|||
expect(first, equals(second));
|
||||
});
|
||||
|
||||
test('hash is exactly 4 bytes long', () {
|
||||
final hash = MessageRetryService.computeExpectedAckHash(
|
||||
fixedTs,
|
||||
0,
|
||||
fixedText,
|
||||
fixedKey,
|
||||
);
|
||||
expect(hash.length, equals(4));
|
||||
});
|
||||
|
||||
test('hash matches manual SHA-256 computation', () {
|
||||
for (int attempt = 0; attempt < 4; attempt++) {
|
||||
final actual = MessageRetryService.computeExpectedAckHash(
|
||||
|
|
@ -509,7 +500,7 @@ void main() {
|
|||
fixedText,
|
||||
fixedKey,
|
||||
);
|
||||
final hex = hash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
final hex = hash.toRadixString(16).padLeft(8, '0');
|
||||
expect(
|
||||
hashes.containsKey(hex),
|
||||
isFalse,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue