mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-24 19:32:52 +10:00
Merge pull request #251 from zjs81/dev-discoverScreen
Contact discovery
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:meshcore_open/models/discovery_contact.dart';
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
@@ -24,6 +25,7 @@ import '../storage/channel_message_store.dart';
|
||||
import '../storage/channel_order_store.dart';
|
||||
import '../storage/channel_settings_store.dart';
|
||||
import '../storage/channel_store.dart';
|
||||
import '../storage/contact_discovery_store.dart';
|
||||
import '../storage/contact_settings_store.dart';
|
||||
import '../storage/contact_store.dart';
|
||||
import '../storage/message_store.dart';
|
||||
@@ -111,6 +113,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
final List<ScanResult> _scanResults = [];
|
||||
final List<Contact> _contacts = [];
|
||||
final List<DiscoveryContact> _discoveredContacts = [];
|
||||
final List<Channel> _channels = [];
|
||||
final Map<String, List<Message>> _conversations = {};
|
||||
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
||||
@@ -155,6 +158,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _batteryRequested = false;
|
||||
bool _awaitingSelfInfo = false;
|
||||
bool _preserveContactsOnRefresh = false;
|
||||
bool _autoAddUsers = false;
|
||||
bool _autoAddRepeaters = false;
|
||||
bool _autoAddRoomServers = false;
|
||||
bool _autoAddSensors = false;
|
||||
bool _overwriteOldest = false;
|
||||
bool _manualAddContacts = false;
|
||||
int _telemetryModeBase = 0;
|
||||
int _telemetryModeLoc = 0;
|
||||
int _telemetryModeEnv = 0;
|
||||
int _advertLocPolicy = 0;
|
||||
int _multiAcks = 0;
|
||||
|
||||
static const int _defaultMaxContacts = 32;
|
||||
static const int _defaultMaxChannels = 8;
|
||||
int _maxContacts = _defaultMaxContacts;
|
||||
@@ -195,6 +210,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
|
||||
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
|
||||
final ContactStore _contactStore = ContactStore();
|
||||
final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore();
|
||||
final ChannelStore _channelStore = ChannelStore();
|
||||
final UnreadStore _unreadStore = UnreadStore();
|
||||
List<Channel> _cachedChannels = [];
|
||||
@@ -242,6 +258,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
List<DiscoveryContact> get discoveredContacts {
|
||||
return List.unmodifiable(_discoveredContacts);
|
||||
}
|
||||
|
||||
List<Channel> get channels => List.unmodifiable(_channels);
|
||||
bool get isConnected => _state == MeshCoreConnectionState.connected;
|
||||
bool get isLoadingContacts => _isLoadingContacts;
|
||||
@@ -258,12 +278,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int? get currentBwHz => _currentBwHz;
|
||||
int? get currentSf => _currentSf;
|
||||
int? get currentCr => _currentCr;
|
||||
bool? get autoAddUsers => _autoAddUsers;
|
||||
bool? get autoAddRepeaters => _autoAddRepeaters;
|
||||
bool? get autoAddRoomServers => _autoAddRoomServers;
|
||||
bool? get autoAddSensors => _autoAddSensors;
|
||||
bool? get autoAddOverwriteOldest => _overwriteOldest;
|
||||
bool? get clientRepeat => _clientRepeat;
|
||||
int? get firmwareVerCode => _firmwareVerCode;
|
||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||
int? get batteryMillivolts => _batteryMillivolts;
|
||||
int get maxContacts => _maxContacts;
|
||||
int get maxChannels => _maxChannels;
|
||||
Set<String> get knownContactKeys => Set.unmodifiable(_knownContactKeys);
|
||||
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
|
||||
bool get isSyncingChannels => _isSyncingChannels;
|
||||
int get channelSyncProgress =>
|
||||
@@ -602,6 +628,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadDiscoveredContactCache() async {
|
||||
final cached = await _discoveryContactStore.loadContacts();
|
||||
_discoveredContacts
|
||||
..clear()
|
||||
..addAll(cached);
|
||||
}
|
||||
|
||||
Future<void> loadChannelSettings({int? maxChannels}) async {
|
||||
_channelSmazEnabled.clear();
|
||||
final channelCount = maxChannels ?? _maxChannels;
|
||||
@@ -848,6 +881,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Fetch channels so we can track unread counts for incoming messages
|
||||
unawaited(getChannels());
|
||||
|
||||
// Load discovered contacts from storage
|
||||
unawaited(loadDiscoveredContactCache());
|
||||
} catch (e) {
|
||||
debugPrint("Connection error: $e");
|
||||
await disconnect(manual: false);
|
||||
@@ -968,6 +1004,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_deviceDisplayName = null;
|
||||
_deviceId = null;
|
||||
_contacts.clear();
|
||||
_discoveredContacts.clear();
|
||||
_conversations.clear();
|
||||
_loadedConversationKeys.clear();
|
||||
_selfPublicKey = null;
|
||||
@@ -1058,8 +1095,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(buildDeviceQueryFrame());
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await requestBatteryStatus(force: true);
|
||||
await sendFrame(buildGetRadioSettingsFrame());
|
||||
await sendFrame(buildGetCustomVarsFrame());
|
||||
await sendFrame(buildGetAutoAddFlagsFrame());
|
||||
|
||||
_scheduleSelfInfoRetry();
|
||||
}
|
||||
|
||||
@@ -1070,7 +1108,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await sendFrame(buildGetCustomVarsFrame());
|
||||
await requestBatteryStatus();
|
||||
|
||||
await sendFrame(buildGetAutoAddFlagsFrame());
|
||||
_scheduleSelfInfoRetry();
|
||||
}
|
||||
|
||||
@@ -1485,6 +1523,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Future<void> removeContact(Contact contact) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
_handleDiscovery(contact, Uint8List(0), noNotify: true);
|
||||
|
||||
await sendFrame(buildRemoveContactFrame(contact.publicKey));
|
||||
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
||||
_knownContactKeys.remove(contact.publicKeyHex);
|
||||
@@ -1499,6 +1539,42 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeDiscoveredContact(DiscoveryContact contact) async {
|
||||
if (!isConnected) return;
|
||||
_discoveredContacts.removeWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
);
|
||||
unawaited(_persistDiscoveredContacts());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> importDiscoveredContact(DiscoveryContact contact) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
await sendFrame(
|
||||
buildUpdateContactPathFrame(
|
||||
contact.publicKey,
|
||||
contact.path,
|
||||
contact.pathLength,
|
||||
type: contact.type,
|
||||
flags: 0,
|
||||
name: contact.name,
|
||||
),
|
||||
);
|
||||
|
||||
_handleContactAdvert(
|
||||
Contact(
|
||||
publicKey: contact.publicKey,
|
||||
name: contact.name,
|
||||
type: contact.type,
|
||||
pathLength: contact.pathLength,
|
||||
path: contact.path,
|
||||
lastSeen: DateTime.now(),
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> clearContactPath(Contact contact) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
@@ -1899,8 +1975,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
case respCodeChannelInfo:
|
||||
_handleChannelInfo(frame);
|
||||
break;
|
||||
case respCodeRadioSettings:
|
||||
_handleRadioSettings(frame);
|
||||
case respCodeAutoAddConfig:
|
||||
_handleAutoAddConfig(frame);
|
||||
_checkManualAddContacts();
|
||||
break;
|
||||
case respCodeBattAndStorage:
|
||||
_handleBatteryAndStorage(frame);
|
||||
@@ -1973,25 +2050,35 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// [56] = sf
|
||||
// [57] = cr
|
||||
// [58+] = node_name
|
||||
if (frame.length < 4 + pubKeySize) return;
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
reader.skipBytes(2);
|
||||
_currentTxPower = reader.readByte();
|
||||
_maxTxPower = reader.readByte();
|
||||
_selfPublicKey = reader.readBytes(pubKeySize);
|
||||
_selfLatitude = reader.readInt32LE() / 1000000.0;
|
||||
_selfLongitude = reader.readInt32LE() / 1000000.0;
|
||||
_multiAcks = reader.readByte();
|
||||
_advertLocPolicy = reader.readByte();
|
||||
|
||||
_currentTxPower = frame[2];
|
||||
_maxTxPower = frame[3];
|
||||
_selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize));
|
||||
_selfLatitude = readInt32LE(frame, 36) / 1000000.0;
|
||||
_selfLongitude = readInt32LE(frame, 40) / 1000000.0;
|
||||
final telemetryFlag = reader.readByte();
|
||||
_telemetryModeBase = telemetryFlag & 0x03;
|
||||
_telemetryModeEnv = telemetryFlag >> 2 & 0x03;
|
||||
_telemetryModeLoc = telemetryFlag >> 4 & 0x03;
|
||||
|
||||
// Radio settings (if frame is long enough)
|
||||
if (frame.length >= 58) {
|
||||
_currentFreqHz = readUint32LE(frame, 48);
|
||||
_currentBwHz = readUint32LE(frame, 52);
|
||||
_currentSf = frame[56];
|
||||
_currentCr = frame[57];
|
||||
}
|
||||
_manualAddContacts = reader.readByte() & 0x01 == 0x00;
|
||||
|
||||
// Node name starts at offset 58 if frame is long enough
|
||||
if (frame.length > 58) {
|
||||
_selfName = readCString(frame, 58, frame.length - 58);
|
||||
_currentFreqHz = reader.readUInt32LE();
|
||||
_currentBwHz = reader.readUInt32LE();
|
||||
_currentSf = reader.readByte();
|
||||
_currentCr = reader.readByte();
|
||||
|
||||
_selfName = reader.readString();
|
||||
} catch (e) {
|
||||
_appDebugLogService?.error(
|
||||
'Error parsing SELF_INFO frame: $e',
|
||||
tag: 'Connector',
|
||||
);
|
||||
}
|
||||
_awaitingSelfInfo = false;
|
||||
_selfInfoRetryTimer?.cancel();
|
||||
@@ -2052,25 +2139,6 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(_requestNextQueuedMessage());
|
||||
}
|
||||
|
||||
void _handleRadioSettings(Uint8List frame) {
|
||||
// Frame format from C++:
|
||||
// [0] = RESP_CODE_RADIO_SETTINGS
|
||||
// [1-4] = freq (uint32 LE, in Hz)
|
||||
// [5-8] = bw (uint32 LE, in Hz)
|
||||
// [9] = sf
|
||||
// [10] = cr
|
||||
if (frame.length >= 11) {
|
||||
_currentFreqHz = readUint32LE(frame, 1);
|
||||
_currentBwHz = readUint32LE(frame, 5);
|
||||
_currentSf = frame[9];
|
||||
_currentCr = frame[10];
|
||||
debugPrint(
|
||||
'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr',
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleBatteryAndStorage(Uint8List frame) {
|
||||
// Frame format from C++:
|
||||
// [0] = RESP_CODE_BATT_AND_STORAGE
|
||||
@@ -2088,6 +2156,32 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _checkManualAddContacts() async {
|
||||
// If manual add contacts is enabled, set auto add config and other params.
|
||||
// and disable it after
|
||||
if (_manualAddContacts) {
|
||||
await sendFrame(
|
||||
buildSetAutoAddConfigFrame(
|
||||
autoAddChat: true,
|
||||
autoAddRepeater: true,
|
||||
autoAddRoomServer: true,
|
||||
autoAddSensor: true,
|
||||
overwriteOldest: _overwriteOldest,
|
||||
),
|
||||
);
|
||||
await sendFrame(
|
||||
buildSetOtherParamsFrame(
|
||||
(_telemetryModeEnv << 4) |
|
||||
(_telemetryModeLoc << 2) |
|
||||
(_telemetryModeBase),
|
||||
_advertLocPolicy,
|
||||
_multiAcks,
|
||||
),
|
||||
);
|
||||
_manualAddContacts = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate timeout for a message based on radio settings and path length
|
||||
/// Returns timeout in milliseconds, considering number of hops
|
||||
int calculateTimeout({required int pathLength, int messageBytes = 100}) {
|
||||
@@ -2271,6 +2365,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await _contactStore.saveContacts(_contacts);
|
||||
}
|
||||
|
||||
Future<void> _persistDiscoveredContacts() async {
|
||||
await _discoveryContactStore.saveContacts(_discoveredContacts);
|
||||
}
|
||||
|
||||
int _latestContactLastmod() {
|
||||
if (_contacts.isEmpty) return 0;
|
||||
var latest = 0;
|
||||
@@ -3705,22 +3803,99 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
|
||||
final rawPacket = frame.sublist(3);
|
||||
switch (payloadType) {
|
||||
case payloadTypeADVERT:
|
||||
_handlePayloadAdvertReceived(payload, pathBytes, routeType, snr);
|
||||
_handlePayloadAdvertReceived(
|
||||
rawPacket,
|
||||
payload,
|
||||
pathBytes,
|
||||
routeType,
|
||||
snr,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
void importContact(Uint8List frame) {
|
||||
final packet = BufferReader(frame);
|
||||
int payloadType = 0;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
try {
|
||||
packet.skipBytes(1); // Skip frame type byte
|
||||
packet.skipBytes(1); // Skip SNR byte
|
||||
packet.skipBytes(1); // Skip RSSI byte
|
||||
final header = packet.readByte();
|
||||
payloadType = (header >> 2) & 0x0F;
|
||||
//final payloadVer = (header >> 6) & 0x03;
|
||||
final pathLen = packet.readByte();
|
||||
pathBytes = packet.readBytes(pathLen);
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
double latitude = 0.0;
|
||||
double longitude = 0.0;
|
||||
String name = '';
|
||||
Uint8List publicKey = Uint8List(0);
|
||||
int type = 0;
|
||||
int timestamp = 0;
|
||||
bool hasLocation = false;
|
||||
bool hasName = false;
|
||||
if (payloadType != payloadTypeADVERT) {
|
||||
appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
publicKey = packet.readBytes(32);
|
||||
timestamp = packet.readInt32LE();
|
||||
//TODO add signature verification
|
||||
packet.skipBytes(64); // Skip signature for now
|
||||
final flags = packet.readByte();
|
||||
type = flags & 0x0F;
|
||||
hasLocation = (flags & 0x10) != 0;
|
||||
// For future use:
|
||||
//final hasFeature1 = (flags & 0x20) != 0;
|
||||
//final hasFeature2 = (flags & 0x40) != 0;
|
||||
hasName = (flags & 0x80) != 0;
|
||||
if (hasLocation && packet.remaining >= 8) {
|
||||
latitude = packet.readInt32LE() / 1e6;
|
||||
longitude = packet.readInt32LE() / 1e6;
|
||||
}
|
||||
if (hasName && packet.remaining > 0) {
|
||||
name = packet.readString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
return;
|
||||
}
|
||||
|
||||
importDiscoveredContact(
|
||||
DiscoveryContact(
|
||||
rawPacket: frame,
|
||||
publicKey: publicKey,
|
||||
name: name,
|
||||
type: type,
|
||||
pathLength: pathBytes.length,
|
||||
path: Uint8List.fromList(
|
||||
pathBytes.reversed.toList(),
|
||||
), // Store path in reverse for easier use in outgoing messages
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handlePayloadAdvertReceived(
|
||||
Uint8List frame,
|
||||
Uint8List rawPacket,
|
||||
Uint8List payload,
|
||||
Uint8List path,
|
||||
int routeType,
|
||||
double snr,
|
||||
) {
|
||||
final advert = BufferReader(frame);
|
||||
final advert = BufferReader(payload);
|
||||
double latitude = 0.0;
|
||||
double longitude = 0.0;
|
||||
String name = '';
|
||||
@@ -3758,6 +3933,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
//We ignore our own adverts
|
||||
if (listEquals(publicKey, _selfPublicKey)) {
|
||||
return;
|
||||
}
|
||||
@@ -3778,7 +3954,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
longitude: longitude,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||
);
|
||||
_handleContactAdvert(newContact);
|
||||
if ((_autoAddUsers && type == advTypeChat) ||
|
||||
(_autoAddRepeaters && type == advTypeRepeater) ||
|
||||
(_autoAddRoomServers && type == advTypeRoom) ||
|
||||
(_autoAddSensors && type == advTypeSensor)) {
|
||||
_handleContactAdvert(newContact);
|
||||
} else {
|
||||
_handleDiscovery(newContact, rawPacket);
|
||||
}
|
||||
_updateDirectRepeater(newContact, snr, path);
|
||||
return;
|
||||
}
|
||||
@@ -3866,6 +4049,84 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleAutoAddConfig(Uint8List frame) {
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
reader.skipBytes(1); // Skip the response code byte
|
||||
final flags = reader.readByte();
|
||||
_autoAddUsers = flags & autoAddChatFlag != 0;
|
||||
_autoAddRepeaters = flags & autoAddRepeaterFlag != 0;
|
||||
_autoAddRoomServers = flags & autoAddRoomServerFlag != 0;
|
||||
_autoAddSensors = flags & autoAddSensorFlag != 0;
|
||||
_overwriteOldest = flags & autoAddOverwriteOldestFlag != 0;
|
||||
} catch (e) {
|
||||
appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector');
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDiscovery(
|
||||
Contact contact,
|
||||
Uint8List rawPacket, {
|
||||
bool noNotify = false,
|
||||
}) {
|
||||
appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector');
|
||||
|
||||
final existingIndex = _discoveredContacts.indexWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
);
|
||||
|
||||
// Update existing contact
|
||||
if (existingIndex >= 0) {
|
||||
_discoveredContacts[existingIndex] = _discoveredContacts[existingIndex]
|
||||
.copyWith(
|
||||
rawPacket: rawPacket,
|
||||
name: contact.name,
|
||||
type: contact.type,
|
||||
pathLength: contact.pathLength,
|
||||
path: contact.path,
|
||||
latitude: contact.latitude,
|
||||
longitude: contact.longitude,
|
||||
lastSeen: contact.lastSeen,
|
||||
);
|
||||
notifyListeners();
|
||||
unawaited(_persistDiscoveredContacts());
|
||||
return;
|
||||
}
|
||||
|
||||
final disContact = DiscoveryContact(
|
||||
rawPacket: rawPacket,
|
||||
publicKey: contact.publicKey,
|
||||
name: contact.name,
|
||||
type: contact.type,
|
||||
pathLength: contact.pathLength,
|
||||
path: contact.path,
|
||||
latitude: contact.latitude,
|
||||
longitude: contact.longitude,
|
||||
lastSeen: contact.lastSeen,
|
||||
);
|
||||
_discoveredContacts.add(disContact);
|
||||
|
||||
unawaited(_persistDiscoveredContacts());
|
||||
|
||||
// Show notification for new contact (advertisement)
|
||||
if (_appSettingsService != null && !noNotify) {
|
||||
final settings = _appSettingsService!.settings;
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||
_notificationService.showAdvertNotification(
|
||||
contactName: contact.name,
|
||||
contactType: contact.typeLabel,
|
||||
contactId: contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void removeAllDiscoveredContacts() {
|
||||
_discoveredContacts.clear();
|
||||
unawaited(_persistDiscoveredContacts());
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
const int _phRouteMask = 0x03;
|
||||
|
||||
@@ -120,25 +120,27 @@ class BufferWriter {
|
||||
}
|
||||
|
||||
void writeHex(String hex) {
|
||||
// Validate hex string length is even and not empty
|
||||
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||
}
|
||||
List<int> result = [];
|
||||
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||
final byte = int.tryParse(hexByte, radix: 16);
|
||||
if (byte == null) {
|
||||
throw FormatException(
|
||||
'Invalid hex characters at position $i: $hexByte',
|
||||
);
|
||||
}
|
||||
result.add(byte);
|
||||
}
|
||||
writeBytes(Uint8List.fromList(result));
|
||||
writeBytes(hex2Uint8List(hex));
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List hex2Uint8List(String hex) {
|
||||
// Validate hex string length is even and not empty
|
||||
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||
}
|
||||
List<int> result = [];
|
||||
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||
final byte = int.tryParse(hexByte, radix: 16);
|
||||
if (byte == null) {
|
||||
throw FormatException('Invalid hex characters at position $i: $hexByte');
|
||||
}
|
||||
result.add(byte);
|
||||
}
|
||||
return Uint8List.fromList(result);
|
||||
}
|
||||
|
||||
// Command codes (to device)
|
||||
const int cmdAppStart = 1;
|
||||
const int cmdSendTxtMsg = 2;
|
||||
@@ -168,11 +170,13 @@ const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
const int cmdGetAutoAddConfig = 59;
|
||||
|
||||
// Text message types
|
||||
const int txtTypePlain = 0;
|
||||
@@ -206,8 +210,8 @@ const int respCodeDeviceInfo = 13;
|
||||
const int respCodeContactMsgRecvV3 = 16;
|
||||
const int respCodeChannelMsgRecvV3 = 17;
|
||||
const int respCodeChannelInfo = 18;
|
||||
const int respCodeRadioSettings = 25;
|
||||
const int respCodeCustomVars = 21;
|
||||
const int respCodeAutoAddConfig = 25;
|
||||
|
||||
// Push codes (async from device)
|
||||
const int pushCodeAdvert = 0x80;
|
||||
@@ -253,6 +257,18 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||
const int payloadTypeRawCustom =
|
||||
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||
|
||||
//auto-add flags
|
||||
const int autoAddOverwriteOldestFlag =
|
||||
1 << 0; // 0x01 - overwrite oldest non-favourite when full
|
||||
const int autoAddChatFlag =
|
||||
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
|
||||
const int autoAddRepeaterFlag =
|
||||
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
|
||||
const int autoAddRoomServerFlag =
|
||||
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
|
||||
const int autoAddSensorFlag =
|
||||
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
|
||||
|
||||
// Sizes
|
||||
const int pubKeySize = 32;
|
||||
const int maxPathSize = 64;
|
||||
@@ -303,7 +319,7 @@ const int contactNameOffset = 100;
|
||||
const int contactTimestampOffset = 132;
|
||||
const int contactLatOffset = 136;
|
||||
const int contactLonOffset = 140;
|
||||
const int contactLastmodOffset = 144;
|
||||
const int contactLastModOffset = 144;
|
||||
const int contactFrameSize = 148;
|
||||
|
||||
// Message frame offsets
|
||||
@@ -681,16 +697,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_GET_RADIO_SETTINGS frame
|
||||
Uint8List buildGetRadioSettingsFrame() {
|
||||
return Uint8List.fromList([cmdGetRadioSettings]);
|
||||
}
|
||||
|
||||
//Build CMD_GET_CUSTOM_VARS frame
|
||||
Uint8List buildGetCustomVarsFrame() {
|
||||
return Uint8List.fromList([cmdGetCustomVar]);
|
||||
}
|
||||
|
||||
Uint8List buildGetAutoAddFlagsFrame() {
|
||||
return Uint8List.fromList([cmdGetAutoAddConfig]);
|
||||
}
|
||||
|
||||
// Calculate LoRa airtime for a packet
|
||||
// Based on Semtech SX127x datasheet formula
|
||||
// Returns airtime in milliseconds
|
||||
@@ -815,10 +830,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
|
||||
|
||||
// Build a import contact frame
|
||||
// [cmd][contact_frame x98+]
|
||||
Uint8List buildImportContactFrame(String contactFrame) {
|
||||
Uint8List buildImportContactFrame(Uint8List contactFrame) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdImportContact);
|
||||
writer.writeHex(contactFrame);
|
||||
writer.writeBytes(contactFrame);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
@@ -832,20 +847,40 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
||||
}
|
||||
|
||||
// Build CMD_SET_OTHER_PARAMS frame
|
||||
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||
Uint8List buildSetOtherParamsFrame(
|
||||
bool allowAutoAddContacts,
|
||||
int allowTelemetryFlags,
|
||||
int advertLocationPolicy,
|
||||
int multiAcks,
|
||||
) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetOtherParams);
|
||||
writer.writeByte(
|
||||
allowAutoAddContacts ? 0x00 : 0x01,
|
||||
); // Allow Auto Add Contacts
|
||||
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
|
||||
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
|
||||
writer.writeByte(0x01);
|
||||
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_SET_AUTO_ADD_CONFIG frame
|
||||
// Format: [cmd][flags]
|
||||
Uint8List buildSetAutoAddConfigFrame({
|
||||
required bool autoAddChat,
|
||||
required bool autoAddRepeater,
|
||||
required bool autoAddRoomServer,
|
||||
required bool autoAddSensor,
|
||||
required bool overwriteOldest,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetAutoAddConfig);
|
||||
int flags = 0;
|
||||
if (autoAddChat) flags |= autoAddChatFlag;
|
||||
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
|
||||
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
|
||||
if (autoAddSensor) flags |= autoAddSensorFlag;
|
||||
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
|
||||
writer.writeByte(flags);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user