meshcore-open/lib/models/contact.dart
zach e7a5b9e209 Initial commit: MeshCore Open Flutter client
Open-source Flutter client for MeshCore LoRa mesh networking devices.

Features:
- BLE device scanning and connection
- Nordic UART Service (NUS) integration
- Material 3 design with system theme support
- Provider-based state management
- Placeholder screens for chat, contacts, and settings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 11:42:02 -07:00

110 lines
3 KiB
Dart

import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
class Contact {
final Uint8List publicKey;
final String name;
final int type;
final int pathLength; // -1 = flood, 0+ = direct hops
final Uint8List path;
final double? latitude;
final double? longitude;
final DateTime lastSeen;
Contact({
required this.publicKey,
required this.name,
required this.type,
required this.pathLength,
required this.path,
this.latitude,
this.longitude,
required this.lastSeen,
});
String get publicKeyHex => pubKeyToHex(publicKey);
String get typeLabel {
switch (type) {
case advTypeChat:
return 'Chat';
case advTypeRepeater:
return 'Repeater';
case advTypeRoom:
return 'Room';
case advTypeSensor:
return 'Sensor';
default:
return 'Unknown';
}
}
String get pathLabel {
if (pathLength < 0) return 'Flood';
if (pathLength == 0) return 'Direct';
return '$pathLength hops';
}
bool get hasLocation => latitude != null && longitude != null;
String get pathIdList {
if (path.isEmpty) return '';
final parts = <String>[];
final groupSize = pathHashSize;
for (int i = 0; i < path.length; i += groupSize) {
final end = (i + groupSize) <= path.length ? (i + groupSize) : path.length;
final chunk = path.sublist(i, end);
parts.add(
chunk.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(),
);
}
return parts.join(',');
}
static Contact? fromFrame(Uint8List data) {
if (data.length < contactFrameSize) return null;
if (data[0] != respCodeContact) return null;
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastmodOffset);
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
}
return Contact(
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
pathLength: pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
);
}
@override
bool operator ==(Object other) =>
other is Contact && publicKeyHex == other.publicKeyHex;
@override
int get hashCode => publicKeyHex.hashCode;
}