mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-19 08:55:33 +10:00
Merge branch 'main' into dev-mapOverlap
This commit is contained in:
@@ -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;
|
||||
@@ -338,6 +341,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
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);
|
||||
@@ -2122,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(
|
||||
@@ -2896,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',
|
||||
@@ -3037,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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3540,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;
|
||||
@@ -3702,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3774,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;
|
||||
@@ -3801,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--) {
|
||||
@@ -3880,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
|
||||
|
||||
@@ -3894,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--) {
|
||||
@@ -3909,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;
|
||||
|
||||
@@ -3930,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();
|
||||
@@ -4284,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) {
|
||||
@@ -4732,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');
|
||||
}
|
||||
@@ -4889,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');
|
||||
@@ -4965,7 +4995,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
hasLocation = hasValidLocation(latitude, longitude);
|
||||
|
||||
if (hasName && advert.remaining > 0) {
|
||||
name = advert.readString();
|
||||
name = advert.readCString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -314,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;
|
||||
@@ -377,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
|
||||
@@ -445,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
|
||||
|
||||
Reference in New Issue
Block a user