Add CompassSarList widget and refactor DetailedCompassDialog

- Introduced CompassSarList widget to display filtered SAR markers with distance and bearing information.
- Refactored DetailedCompassDialog to integrate CompassSarList and CompassContactList for better organization.
- Removed the previous filter dialog implementation and replaced it with a more modular CompassFilters widget.
- Simplified the handling of zoom and scale updates in the compass view.
- Cleaned up unused code related to previous implementations of contact and SAR marker lists.
This commit is contained in:
Janez T
2025-10-15 10:30:21 +02:00
parent 8af96c3fec
commit e831612a1a
15 changed files with 3261 additions and 3055 deletions

View File

@@ -0,0 +1,238 @@
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 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 (deprecated)
@Deprecated('Use buildSendBinaryReq() instead')
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();
}
}

View File

@@ -0,0 +1,398 @@
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.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();
}
return Message(
id: '${DateTime.now().millisecondsSinceEpoch}_ch$channelIdx',
messageType: MessageType.channel,
channelIdx: channelIdx,
pathLen: pathLen,
textType: txtType,
senderTimestamp: senderTimestamp,
text: text,
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);
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<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;
}
/// 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';
}
}
}