mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-28 14:36:39 +10:00
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>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user