mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 17:00:28 +00:00
feat: MeshCore SAR - Flutter BLE mesh radio companion app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
292
lib/services/protocol/frame_builder.dart
Normal file
292
lib/services/protocol/frame_builder.dart
Normal file
@@ -0,0 +1,292 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import '../../models/contact.dart';
|
||||
import '../buffer_writer.dart';
|
||||
import '../meshcore_constants.dart';
|
||||
|
||||
/// Builds outgoing BLE frames for the MeshCore device
|
||||
class FrameBuilder {
|
||||
/// Build DeviceQuery command
|
||||
static Uint8List buildDeviceQuery() {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdDeviceQuery);
|
||||
writer.writeByte(MeshCoreConstants.supportedCompanionProtocolVersion);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build AppStart command
|
||||
static Uint8List buildAppStart() {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdAppStart);
|
||||
writer.writeByte(1); // appVer
|
||||
writer.writeBytes(Uint8List(6)); // reserved
|
||||
writer.writeString('MeshCore SAR'); // appName
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build GetContacts command
|
||||
static Uint8List buildGetContacts() {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdGetContacts);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build GetContactByKey command - retrieves a single contact by public key
|
||||
static Uint8List buildGetContactByKey(Uint8List publicKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdGetContactByKey); // 0x1E (30)
|
||||
writer.writeBytes(publicKey); // 32 bytes
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build AddUpdateContact command
|
||||
static Uint8List buildAddUpdateContact(Contact contact) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdAddUpdateContact); // 0x09
|
||||
writer.writeBytes(contact.publicKey); // 32 bytes
|
||||
writer.writeByte(contact.type.value); // ADV_TYPE_*
|
||||
writer.writeByte(contact.flags); // flags
|
||||
writer.writeInt8(contact.outPathLen); // path length (signed byte)
|
||||
writer.writeBytes(contact.outPath); // 64 bytes
|
||||
|
||||
// Write name as null-terminated string in 32-byte field
|
||||
final nameBytes = Uint8List(32);
|
||||
final encoded = utf8.encode(contact.advName);
|
||||
final copyLen = encoded.length > 31 ? 31 : encoded.length;
|
||||
nameBytes.setRange(0, copyLen, encoded);
|
||||
writer.writeBytes(nameBytes);
|
||||
|
||||
writer.writeUInt32LE(contact.lastAdvert); // timestamp
|
||||
writer.writeInt32LE(contact.advLat); // latitude * 1E6
|
||||
writer.writeInt32LE(contact.advLon); // longitude * 1E6
|
||||
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SendTxtMsg command
|
||||
static Uint8List buildSendTxtMsg({
|
||||
required Uint8List contactPublicKey,
|
||||
required String text,
|
||||
int textType = 0,
|
||||
int attempt = 0,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendTxtMsg); // 0x02
|
||||
writer.writeByte(textType); // TXT_TYPE_*
|
||||
writer.writeByte(attempt); // 0-3
|
||||
writer.writeUInt32LE(DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
writer.writeBytes(contactPublicKey.sublist(0, 6));
|
||||
writer.writeString(text);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SendChannelTxtMsg command
|
||||
static Uint8List buildSendChannelTxtMsg({
|
||||
required int channelIdx,
|
||||
required String text,
|
||||
int textType = 0,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendChannelTxtMsg); // 0x03
|
||||
writer.writeByte(textType); // TXT_TYPE_*
|
||||
writer.writeByte(channelIdx); // 0 for 'public' channel
|
||||
writer.writeUInt32LE(DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
writer.writeString(text);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SendTelemetryReq command
|
||||
/// Requests telemetry (GPS, battery) from a contact
|
||||
static Uint8List buildSendTelemetryReq(Uint8List contactPublicKey, {bool zeroHop = false}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendTelemetryReq);
|
||||
writer.writeByte(zeroHop ? 0 : 255);
|
||||
writer.writeByte(0); // reserved
|
||||
writer.writeByte(0); // reserved
|
||||
writer.writeBytes(contactPublicKey);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SendBinaryReq command
|
||||
static Uint8List buildSendBinaryReq({
|
||||
required Uint8List contactPublicKey,
|
||||
required Uint8List requestData,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendBinaryReq); // 0x32 (50)
|
||||
writer.writeBytes(contactPublicKey); // 32 bytes
|
||||
writer.writeBytes(requestData); // request code + params
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build GetBatteryVoltage command
|
||||
static Uint8List buildGetBatteryAndStorage() {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdGetBatteryVoltage);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SyncNextMessage command
|
||||
static Uint8List buildSyncNextMessage() {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSyncNextMessage);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build GetDeviceTime command
|
||||
static Uint8List buildGetDeviceTime() {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdGetDeviceTime);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SetDeviceTime command
|
||||
static Uint8List buildSetDeviceTime() {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSetDeviceTime);
|
||||
writer.writeUInt32LE(DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SendSelfAdvert command
|
||||
static Uint8List buildSendSelfAdvert({bool floodMode = true}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendSelfAdvert);
|
||||
writer.writeByte(floodMode ? MeshCoreConstants.selfAdvertFlood : MeshCoreConstants.selfAdvertZeroHop);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SetAdvertName command
|
||||
static Uint8List buildSetAdvertName(String name) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSetAdvertName);
|
||||
writer.writeString(name);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SetAdvertLatLon command
|
||||
static Uint8List buildSetAdvertLatLon({
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSetAdvertLatLon);
|
||||
writer.writeInt32LE((latitude * 1000000).round());
|
||||
writer.writeInt32LE((longitude * 1000000).round());
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SetRadioParams command
|
||||
static Uint8List buildSetRadioParams({
|
||||
required int frequency,
|
||||
required int bandwidth,
|
||||
required int spreadingFactor,
|
||||
required int codingRate,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSetRadioParams);
|
||||
writer.writeUInt32LE(frequency);
|
||||
writer.writeUInt16LE(bandwidth);
|
||||
writer.writeByte(spreadingFactor);
|
||||
writer.writeByte(codingRate);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SetTxPower command
|
||||
static Uint8List buildSetTxPower(int powerDbm) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSetTxPower);
|
||||
writer.writeByte(powerDbm);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SetOtherParams command
|
||||
static Uint8List buildSetOtherParams({
|
||||
required int manualAddContacts,
|
||||
required int telemetryModes,
|
||||
required int advertLocationPolicy,
|
||||
int multiAcks = 0,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSetOtherParams);
|
||||
writer.writeByte(manualAddContacts);
|
||||
writer.writeByte(telemetryModes);
|
||||
writer.writeByte(advertLocationPolicy);
|
||||
writer.writeByte(multiAcks);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SendLogin command
|
||||
static Uint8List buildSendLogin({
|
||||
required Uint8List roomPublicKey,
|
||||
required String password,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendLogin); // 0x1A
|
||||
writer.writeBytes(roomPublicKey); // 32 bytes
|
||||
writer.writeString(password); // Max 15 bytes, null-terminated
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SendStatusReq command
|
||||
static Uint8List buildSendStatusReq(Uint8List contactPublicKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSendStatusReq); // 0x1B
|
||||
writer.writeBytes(contactPublicKey); // 32 bytes
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build ResetPath command - clears learned path for a contact
|
||||
static Uint8List buildResetPath(Uint8List contactPublicKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdResetPath); // 0x0D (13)
|
||||
writer.writeBytes(contactPublicKey); // 32 bytes
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build RemoveContact command - removes a contact from the device
|
||||
static Uint8List buildRemoveContact(Uint8List contactPublicKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdRemoveContact); // 0x0F (15)
|
||||
writer.writeBytes(contactPublicKey); // 32 bytes
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build GetChannel command - retrieves information for a specific channel
|
||||
static Uint8List buildGetChannel(int channelIdx) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdGetChannel); // 0x1F (31)
|
||||
writer.writeByte(channelIdx); // 0-39 typically
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
/// Build SetChannel command - sets the name and secret for a specific channel
|
||||
///
|
||||
/// Format: [cmd(1)][channel_idx(1)][name(32)][secret(16)]
|
||||
/// Secret must be exactly 16 bytes (128-bit key)
|
||||
static Uint8List buildSetChannel({
|
||||
required int channelIdx,
|
||||
required String channelName,
|
||||
required List<int> secret,
|
||||
}) {
|
||||
if (secret.length != 16) {
|
||||
throw ArgumentError('Channel secret must be exactly 16 bytes (got ${secret.length})');
|
||||
}
|
||||
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(MeshCoreConstants.cmdSetChannel); // 0x20 (32)
|
||||
writer.writeByte(channelIdx); // 0-39 typically
|
||||
|
||||
// Write channel name as null-terminated string in 32-byte field
|
||||
final nameBytes = Uint8List(32);
|
||||
final encoded = utf8.encode(channelName);
|
||||
final copyLen = encoded.length > 31 ? 31 : encoded.length;
|
||||
nameBytes.setRange(0, copyLen, encoded);
|
||||
writer.writeBytes(nameBytes);
|
||||
|
||||
// Write 16-byte secret
|
||||
writer.writeBytes(Uint8List.fromList(secret));
|
||||
|
||||
return writer.toBytes();
|
||||
}
|
||||
}
|
||||
436
lib/services/protocol/frame_parser.dart
Normal file
436
lib/services/protocol/frame_parser.dart
Normal file
@@ -0,0 +1,436 @@
|
||||
import 'dart:convert';
|
||||
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<String, dynamic> 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.readByte(); // unsigned 0-255, not signed
|
||||
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: "<sender_name>: <actual_message>"
|
||||
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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);
|
||||
|
||||
reader.readByte(); // multiAcks (reserved for future use)
|
||||
reader.readByte(); // advertLocPolicy (reserved for future use)
|
||||
reader.readByte(); // telemetryModes (reserved for future use)
|
||||
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 = utf8.decode(nameBytes.takeWhile((b) => b != 0).toList());
|
||||
}
|
||||
|
||||
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
|
||||
/// Parse ChannelInfo response
|
||||
static Map<String, dynamic> parseChannelInfo(BufferReader reader) {
|
||||
// Format: [channel_idx(1)][name(32)][secret(16)][flags(1)?]
|
||||
// Minimum: 1 + 32 + 16 = 49 bytes (flags is optional)
|
||||
if (reader.remainingBytesCount < 49) {
|
||||
return {};
|
||||
}
|
||||
|
||||
final channelIdx = reader.readByte();
|
||||
final channelName = reader.readCString(32);
|
||||
final secret = reader.readBytes(16);
|
||||
|
||||
// Flags field is optional (some firmware versions don't include it)
|
||||
int? flags;
|
||||
if (reader.remainingBytesCount >= 1) {
|
||||
flags = reader.readByte();
|
||||
}
|
||||
|
||||
return {
|
||||
'channelIdx': channelIdx,
|
||||
'channelName': channelName,
|
||||
'secret': secret,
|
||||
'flags': flags,
|
||||
};
|
||||
}
|
||||
|
||||
/// 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user