import 'dart:typed_data'; import '../../models/contact.dart'; import '../../models/message.dart'; import '../buffer_reader.dart'; import '../meshcore_constants.dart'; /// Parses incoming BLE frames from the MeshCore device class FrameParser { /// Parse ContactsStart response static int parseContactsStart(BufferReader reader) { return reader.readUInt32LE(); } /// Parse Contact response static Contact parseContact(BufferReader reader) { final publicKey = reader.readBytes(32); final typeByte = reader.readByte(); final type = ContactType.fromValue(typeByte); final flags = reader.readByte(); final outPathLen = reader.readInt8(); final outPath = reader.readBytes(64); final advName = reader.readCString(32); final lastAdvert = reader.readUInt32LE(); final advLat = reader.readInt32LE(); final advLon = reader.readInt32LE(); final lastMod = reader.readUInt32LE(); return Contact( publicKey: publicKey, type: type, flags: flags, outPathLen: outPathLen, outPath: outPath, advName: advName, lastAdvert: lastAdvert, advLat: advLat, advLon: advLon, lastMod: lastMod, ); } /// Parse Sent confirmation response static Map parseSentConfirmation(BufferReader reader) { if (reader.remainingBytesCount >= 9) { final sendType = reader.readByte(); final isFloodMode = sendType == 1; final expectedAckOrTagBytes = reader.readBytes(4); final expectedAckTag = ByteData.sublistView(Uint8List.fromList(expectedAckOrTagBytes)) .getUint32(0, Endian.little); final suggestedTimeout = reader.readUInt32LE(); return { 'expectedAckTag': expectedAckTag, 'suggestedTimeout': suggestedTimeout, 'isFloodMode': isFloodMode, }; } return {}; } /// Parse ContactMessage response static Message parseContactMessage(BufferReader reader) { final pubKeyPrefix = reader.readBytes(6); final pathLen = reader.readByte(); final txtTypeByte = reader.readByte(); final txtType = MessageTextType.fromValue(txtTypeByte); final senderTimestamp = reader.readUInt32LE(); String text; if (txtType == MessageTextType.signedPlain) { // Signed message format: [4-byte sender prefix][UTF-8 text] if (reader.remainingBytesCount >= 4) { reader.readBytes(4); // Skip extra sender prefix text = reader.hasRemaining ? reader.readString() : ''; } else { text = reader.readString(); } } else { text = reader.readString(); } return Message( id: '${DateTime.now().millisecondsSinceEpoch}_${pubKeyPrefix.map((b) => b.toRadixString(16)).join()}', messageType: MessageType.contact, senderPublicKeyPrefix: pubKeyPrefix, pathLen: pathLen, textType: txtType, senderTimestamp: senderTimestamp, text: text, receivedAt: DateTime.now(), ); } /// Parse ChannelMessage response static Message parseChannelMessage(BufferReader reader) { final channelIdx = reader.readInt8(); final pathLen = reader.readByte(); final txtTypeByte = reader.readByte(); final txtType = MessageTextType.fromValue(txtTypeByte); final senderTimestamp = reader.readUInt32LE(); String text; if (txtType == MessageTextType.signedPlain) { if (reader.remainingBytesCount >= 4) { reader.readBytes(4); // Skip extra sender prefix text = reader.hasRemaining ? reader.readString() : ''; } else { text = reader.readString(); } } else { text = reader.readString(); } // Parse sender name from channel message format: ": " String? senderName; String actualMessage = text; if (text.contains(': ')) { final colonIndex = text.indexOf(': '); senderName = text.substring(0, colonIndex); actualMessage = text.substring(colonIndex + 2); // Skip ": " } return Message( id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx', messageType: MessageType.channel, channelIdx: channelIdx, pathLen: pathLen, textType: txtType, senderTimestamp: senderTimestamp, text: actualMessage, // Store the actual message without sender prefix senderName: senderName, // Store extracted sender name receivedAt: DateTime.now(), ); } /// Parse TelemetryResponse push static Map parseTelemetryResponse(BufferReader reader) { reader.readByte(); // reserved final pubKeyPrefix = reader.readBytes(6); final lppSensorData = reader.readRemainingBytes(); return { 'publicKeyPrefix': pubKeyPrefix, 'lppSensorData': lppSensorData, }; } /// Parse BinaryResponse push static Map parseBinaryResponse(BufferReader reader) { reader.readByte(); // reserved final tag = reader.readUInt32LE(); final responseData = reader.readRemainingBytes(); return { 'publicKeyPrefix': Uint8List(6), // Empty prefix 'tag': tag, 'responseData': responseData, }; } /// Parse DeviceInfo response static Map parseDeviceInfo(BufferReader reader) { if (reader.remainingBytesCount < 1) { return {}; } final firmwareVersion = reader.readByte(); int? maxContacts; int? maxChannels; int? blePin; if (reader.remainingBytesCount >= 6) { final maxContactsDiv2 = reader.readByte(); maxContacts = maxContactsDiv2 * 2; maxChannels = reader.readByte(); blePin = reader.readUInt32LE(); } String? firmwareBuildDate; if (reader.remainingBytesCount >= 12) { final buildDateBytes = reader.readBytes(12); firmwareBuildDate = String.fromCharCodes(buildDateBytes.takeWhile((b) => b != 0)); } String? manufacturerModel; if (reader.remainingBytesCount >= 40) { final modelBytes = reader.readBytes(40); manufacturerModel = String.fromCharCodes(modelBytes.takeWhile((b) => b != 0)); } String? semanticVersion; if (reader.remainingBytesCount >= 20) { final versionBytes = reader.readBytes(20); semanticVersion = String.fromCharCodes(versionBytes.takeWhile((b) => b != 0)); } return { 'firmwareVersion': firmwareVersion, 'maxContacts': maxContacts, 'maxChannels': maxChannels, 'blePin': blePin, 'firmwareBuildDate': firmwareBuildDate, 'manufacturerModel': manufacturerModel, 'semanticVersion': semanticVersion, }; } /// Parse SelfInfo response static Map parseSelfInfo(BufferReader reader) { if (reader.remainingBytesCount < 54) { reader.readRemainingBytes(); return {}; } final deviceType = reader.readByte(); final txPower = reader.readByte(); final maxTxPower = reader.readByte(); final publicKey = reader.readBytes(32); final advLatBytes = reader.readBytes(4); final advLat = ByteData.sublistView(Uint8List.fromList(advLatBytes)) .getInt32(0, Endian.little); final advLonBytes = reader.readBytes(4); final advLon = ByteData.sublistView(Uint8List.fromList(advLonBytes)) .getInt32(0, Endian.little); final multiAcks = reader.readByte(); final advertLocPolicy = reader.readByte(); final telemetryModes = reader.readByte(); final manualAddContacts = reader.readByte(); final radioFreqBytes = reader.readBytes(4); final radioFreq = ByteData.sublistView(Uint8List.fromList(radioFreqBytes)) .getUint32(0, Endian.little); final radioBwBytes = reader.readBytes(4); final radioBw = ByteData.sublistView(Uint8List.fromList(radioBwBytes)) .getUint32(0, Endian.little); final radioSf = reader.readByte(); final radioCr = reader.readByte(); String? selfName; if (reader.hasRemaining) { final nameBytes = reader.readRemainingBytes(); selfName = String.fromCharCodes(nameBytes.takeWhile((b) => b != 0)); } return { 'deviceType': deviceType, 'txPower': txPower, 'maxTxPower': maxTxPower, 'publicKey': publicKey, 'advLat': advLat, 'advLon': advLon, 'manualAddContacts': manualAddContacts == 1, 'radioFreq': radioFreq, 'radioBw': radioBw, 'radioSf': radioSf, 'radioCr': radioCr, 'selfName': selfName, }; } /// Parse Advert push static Uint8List? parseAdvert(BufferReader reader) { if (reader.remainingBytesCount >= 32) { return reader.readBytes(32); } return null; } /// Parse PathUpdated push static Uint8List? parsePathUpdated(BufferReader reader) { if (reader.remainingBytesCount >= 32) { return reader.readBytes(32); } return null; } /// Parse SendConfirmed push static Map parseSendConfirmed(BufferReader reader) { if (reader.remainingBytesCount >= 8) { final ackCodeBytes = reader.readBytes(4); final ackCode = ByteData.sublistView(Uint8List.fromList(ackCodeBytes)) .getUint32(0, Endian.little); final roundTripTime = reader.readUInt32LE(); return { 'ackCode': ackCode, 'roundTripTime': roundTripTime, }; } return {}; } /// Parse LoginSuccess push static Map parseLoginSuccess(BufferReader reader) { if (reader.remainingBytesCount >= 11) { final permissions = reader.readByte(); final isAdmin = (permissions & 0x01) != 0; final publicKeyPrefix = reader.readBytes(6); final tag = reader.readInt32LE(); int? newPermissions; if (reader.hasRemaining) { newPermissions = reader.readByte(); } return { 'publicKeyPrefix': publicKeyPrefix, 'permissions': permissions, 'isAdmin': isAdmin, 'tag': tag, 'newPermissions': newPermissions, }; } return {}; } /// Parse LoginFail push static Uint8List? parseLoginFail(BufferReader reader) { if (reader.remainingBytesCount >= 7) { reader.readByte(); // reserved return reader.readBytes(6); } return null; } /// Parse StatusResponse push static Map parseStatusResponse(BufferReader reader) { if (reader.remainingBytesCount >= 7) { reader.readByte(); // reserved final publicKeyPrefix = reader.readBytes(6); final statusData = reader.readRemainingBytes(); return { 'publicKeyPrefix': publicKeyPrefix, 'statusData': statusData, }; } return {}; } /// Parse CurrentTime response static int? parseCurrentTime(BufferReader reader) { if (reader.remainingBytesCount >= 4) { return reader.readUInt32LE(); } return null; } /// Parse BatteryAndStorage response static Map parseBatteryAndStorage(BufferReader reader) { if (reader.remainingBytesCount >= 2) { final millivolts = reader.readUInt16LE(); int? usedKb; int? totalKb; if (reader.remainingBytesCount >= 8) { usedKb = reader.readUInt32LE(); totalKb = reader.readUInt32LE(); } else if (reader.remainingBytesCount >= 4) { usedKb = reader.readUInt32LE(); } return { 'millivolts': millivolts, 'usedKb': usedKb, 'totalKb': totalKb, }; } return {}; } /// Parse Error response static int? parseError(BufferReader reader) { if (reader.hasRemaining) { return reader.readByte(); } return null; } /// Get error message from error code static String getErrorMessage(int errorCode) { switch (errorCode) { case MeshCoreConstants.errUnsupportedCmd: return 'Unsupported command'; case MeshCoreConstants.errNotFound: return 'Not found'; case MeshCoreConstants.errTableFull: return 'Table full'; case MeshCoreConstants.errBadState: return 'Bad state'; case MeshCoreConstants.errFileIoError: return 'File I/O error'; case MeshCoreConstants.errIllegalArg: return 'Illegal argument'; default: return 'Error code: $errorCode'; } } }