mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-17 06:10:47 +10:00
added buildSetCustomVarFrame and setCustomVar
This commit is contained in:
@@ -66,8 +66,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final Map<String, List<Message>> _conversations = {};
|
final Map<String, List<Message>> _conversations = {};
|
||||||
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
||||||
final Set<String> _loadedConversationKeys = {};
|
final Set<String> _loadedConversationKeys = {};
|
||||||
final Map<int, Set<String>> _processedChannelReactions = {}; // channelIndex -> Set of "reactionKey_emoji"
|
final Map<int, Set<String>> _processedChannelReactions =
|
||||||
final Map<String, Set<String>> _processedContactReactions = {}; // contactPubKeyHex -> Set of "reactionKey_emoji"
|
{}; // channelIndex -> Set of "reactionKey_emoji"
|
||||||
|
final Map<String, Set<String>> _processedContactReactions =
|
||||||
|
{}; // contactPubKeyHex -> Set of "reactionKey_emoji"
|
||||||
|
|
||||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||||
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
|
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
|
||||||
@@ -137,7 +139,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final ContactStore _contactStore = ContactStore();
|
final ContactStore _contactStore = ContactStore();
|
||||||
final UnreadStore _unreadStore = UnreadStore();
|
final UnreadStore _unreadStore = UnreadStore();
|
||||||
final Map<int, bool> _channelSmazEnabled = {};
|
final Map<int, bool> _channelSmazEnabled = {};
|
||||||
bool _lastSentWasCliCommand = false; // Track if last sent message was a CLI command
|
bool _lastSentWasCliCommand =
|
||||||
|
false; // Track if last sent message was a CLI command
|
||||||
final Map<String, bool> _contactSmazEnabled = {};
|
final Map<String, bool> _contactSmazEnabled = {};
|
||||||
final Set<String> _knownContactKeys = {};
|
final Set<String> _knownContactKeys = {};
|
||||||
final Map<String, int> _contactLastReadMs = {};
|
final Map<String, int> _contactLastReadMs = {};
|
||||||
@@ -166,6 +169,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
return 'Unknown Device';
|
return 'Unknown Device';
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ScanResult> get scanResults => List.unmodifiable(_scanResults);
|
List<ScanResult> get scanResults => List.unmodifiable(_scanResults);
|
||||||
List<Contact> get contacts {
|
List<Contact> get contacts {
|
||||||
final selfKey = _selfPublicKey;
|
final selfKey = _selfPublicKey;
|
||||||
@@ -176,6 +180,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_contacts.where((contact) => !listEquals(contact.publicKey, selfKey)),
|
_contacts.where((contact) => !listEquals(contact.publicKey, selfKey)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Channel> get channels => List.unmodifiable(_channels);
|
List<Channel> get channels => List.unmodifiable(_channels);
|
||||||
bool get isConnected => _state == MeshCoreConnectionState.connected;
|
bool get isConnected => _state == MeshCoreConnectionState.connected;
|
||||||
bool get isLoadingContacts => _isLoadingContacts;
|
bool get isLoadingContacts => _isLoadingContacts;
|
||||||
@@ -196,7 +201,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int get maxChannels => _maxChannels;
|
int get maxChannels => _maxChannels;
|
||||||
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
|
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
|
||||||
bool get isSyncingChannels => _isSyncingChannels;
|
bool get isSyncingChannels => _isSyncingChannels;
|
||||||
int get channelSyncProgress => _isSyncingChannels && _totalChannelsToRequest > 0
|
int get channelSyncProgress =>
|
||||||
|
_isSyncingChannels && _totalChannelsToRequest > 0
|
||||||
? ((_nextChannelIndexToRequest / _totalChannelsToRequest) * 100).round()
|
? ((_nextChannelIndexToRequest / _totalChannelsToRequest) * 100).round()
|
||||||
: 0;
|
: 0;
|
||||||
int? get batteryPercent => _batteryMillivolts == null
|
int? get batteryPercent => _batteryMillivolts == null
|
||||||
@@ -377,7 +383,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setActiveContact(String? contactKeyHex) {
|
void setActiveContact(String? contactKeyHex) {
|
||||||
if (contactKeyHex != null && !_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
if (contactKeyHex != null &&
|
||||||
|
!_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
||||||
_activeContactKey = null;
|
_activeContactKey = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -429,7 +436,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Load persisted channel messages for a specific channel
|
/// Load persisted channel messages for a specific channel
|
||||||
Future<void> _loadChannelMessages(int channelIndex) async {
|
Future<void> _loadChannelMessages(int channelIndex) async {
|
||||||
final allMessages = await _channelMessageStore.loadChannelMessages(channelIndex);
|
final allMessages = await _channelMessageStore.loadChannelMessages(
|
||||||
|
channelIndex,
|
||||||
|
);
|
||||||
if (allMessages.isNotEmpty) {
|
if (allMessages.isNotEmpty) {
|
||||||
// Keep only the most recent N messages in memory to bound memory usage
|
// Keep only the most recent N messages in memory to bound memory usage
|
||||||
final windowedMessages = allMessages.length > _messageWindowSize
|
final windowedMessages = allMessages.length > _messageWindowSize
|
||||||
@@ -446,7 +455,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int channelIndex, {
|
int channelIndex, {
|
||||||
int count = 50,
|
int count = 50,
|
||||||
}) async {
|
}) async {
|
||||||
final allMessages = await _channelMessageStore.loadChannelMessages(channelIndex);
|
final allMessages = await _channelMessageStore.loadChannelMessages(
|
||||||
|
channelIndex,
|
||||||
|
);
|
||||||
final currentMessages = _channelMessages[channelIndex] ?? [];
|
final currentMessages = _channelMessages[channelIndex] ?? [];
|
||||||
|
|
||||||
if (allMessages.length <= currentMessages.length) {
|
if (allMessages.length <= currentMessages.length) {
|
||||||
@@ -551,7 +562,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final contactKey = pubKeyToHex(message.senderKey);
|
final contactKey = pubKeyToHex(message.senderKey);
|
||||||
final messages = _conversations[contactKey];
|
final messages = _conversations[contactKey];
|
||||||
if (messages != null) {
|
if (messages != null) {
|
||||||
final index = messages.indexWhere((m) => m.messageId == message.messageId);
|
final index = messages.indexWhere(
|
||||||
|
(m) => m.messageId == message.messageId,
|
||||||
|
);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
messages[index] = message;
|
messages[index] = message;
|
||||||
_messageStore.saveMessages(contactKey, messages);
|
_messageStore.saveMessages(contactKey, messages);
|
||||||
@@ -576,7 +589,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Contact _applyAutoSelection(Contact contact, PathSelection? selection) {
|
Contact _applyAutoSelection(Contact contact, PathSelection? selection) {
|
||||||
if (selection == null || selection.useFlood || selection.pathBytes.isEmpty) {
|
if (selection == null ||
|
||||||
|
selection.useFlood ||
|
||||||
|
selection.pathBytes.isEmpty) {
|
||||||
return contact;
|
return contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +599,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
publicKey: contact.publicKey,
|
publicKey: contact.publicKey,
|
||||||
name: contact.name,
|
name: contact.name,
|
||||||
type: contact.type,
|
type: contact.type,
|
||||||
pathLength: selection.hopCount >= 0 ? selection.hopCount : contact.pathLength,
|
pathLength: selection.hopCount >= 0
|
||||||
|
? selection.hopCount
|
||||||
|
: contact.pathLength,
|
||||||
path: Uint8List.fromList(selection.pathBytes),
|
path: Uint8List.fromList(selection.pathBytes),
|
||||||
latitude: contact.latitude,
|
latitude: contact.latitude,
|
||||||
longitude: contact.longitude,
|
longitude: contact.longitude,
|
||||||
@@ -593,7 +610,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startScan({Duration timeout = const Duration(seconds: 10)}) async {
|
Future<void> startScan({
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
}) async {
|
||||||
if (_state == MeshCoreConnectionState.scanning) return;
|
if (_state == MeshCoreConnectionState.scanning) return;
|
||||||
|
|
||||||
_scanResults.clear();
|
_scanResults.clear();
|
||||||
@@ -714,7 +733,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (attempt == 2) rethrow;
|
if (attempt == 2) rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_notifySubscription = _txCharacteristic!.onValueReceived.listen(_handleFrame);
|
_notifySubscription = _txCharacteristic!.onValueReceived.listen(
|
||||||
|
_handleFrame,
|
||||||
|
);
|
||||||
|
|
||||||
_setState(MeshCoreConnectionState.connected);
|
_setState(MeshCoreConnectionState.connected);
|
||||||
|
|
||||||
@@ -771,8 +792,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _shouldAutoReconnect =>
|
bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
|
||||||
!_manualDisconnect && _lastDeviceId != null;
|
|
||||||
|
|
||||||
void _cancelReconnectTimer() {
|
void _cancelReconnectTimer() {
|
||||||
_reconnectTimer?.cancel();
|
_reconnectTimer?.cancel();
|
||||||
@@ -799,7 +819,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final device = _lastDevice ??
|
final device =
|
||||||
|
_lastDevice ??
|
||||||
(_lastDeviceId == null
|
(_lastDeviceId == null
|
||||||
? null
|
? null
|
||||||
: BluetoothDevice.fromId(_lastDeviceId!));
|
: BluetoothDevice.fromId(_lastDeviceId!));
|
||||||
@@ -945,20 +966,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
void _scheduleSelfInfoRetry() {
|
void _scheduleSelfInfoRetry() {
|
||||||
_selfInfoRetryTimer?.cancel();
|
_selfInfoRetryTimer?.cancel();
|
||||||
_selfInfoRetryTimer = Timer.periodic(
|
_selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), (
|
||||||
const Duration(milliseconds: 3500),
|
timer,
|
||||||
(timer) {
|
) {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_awaitingSelfInfo) {
|
if (!_awaitingSelfInfo) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unawaited(sendFrame(buildAppStartFrame()));
|
unawaited(sendFrame(buildAppStartFrame()));
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getContacts({int? since, bool preserveExisting = false}) async {
|
Future<void> getContacts({int? since, bool preserveExisting = false}) async {
|
||||||
@@ -979,10 +999,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshContactsSinceLastmod() async {
|
Future<void> refreshContactsSinceLastmod() async {
|
||||||
await getContacts(
|
await getContacts(since: _latestContactLastmod(), preserveExisting: true);
|
||||||
since: _latestContactLastmod(),
|
|
||||||
preserveExisting: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getContactByKey(Uint8List pubKey) async {
|
Future<void> getContactByKey(Uint8List pubKey) async {
|
||||||
@@ -990,18 +1007,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
await sendFrame(buildGetContactByKeyFrame(pubKey));
|
await sendFrame(buildGetContactByKeyFrame(pubKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendMessage(
|
Future<void> sendMessage(Contact contact, String text) async {
|
||||||
Contact contact,
|
|
||||||
String text,
|
|
||||||
) async {
|
|
||||||
if (!isConnected || text.isEmpty) return;
|
if (!isConnected || text.isEmpty) return;
|
||||||
|
|
||||||
// Handle auto-rotation if enabled
|
// Handle auto-rotation if enabled
|
||||||
PathSelection? autoSelection;
|
PathSelection? autoSelection;
|
||||||
if (_appSettingsService?.settings.autoRouteRotationEnabled == true) {
|
if (_appSettingsService?.settings.autoRouteRotationEnabled == true) {
|
||||||
autoSelection = _pathHistoryService?.getNextAutoPathSelection(contact.publicKeyHex);
|
autoSelection = _pathHistoryService?.getNextAutoPathSelection(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
);
|
||||||
if (autoSelection != null) {
|
if (autoSelection != null) {
|
||||||
_pathHistoryService?.recordPathAttempt(contact.publicKeyHex, autoSelection);
|
_pathHistoryService?.recordPathAttempt(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
autoSelection,
|
||||||
|
);
|
||||||
if (!autoSelection.useFlood && autoSelection.pathBytes.isNotEmpty) {
|
if (!autoSelection.useFlood && autoSelection.pathBytes.isNotEmpty) {
|
||||||
await setContactPath(
|
await setContactPath(
|
||||||
contact,
|
contact,
|
||||||
@@ -1036,12 +1055,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_addMessage(contact.publicKeyHex, message);
|
_addMessage(contact.publicKeyHex, message);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final outboundText = prepareContactOutboundText(contact, text);
|
final outboundText = prepareContactOutboundText(contact, text);
|
||||||
await sendFrame(
|
await sendFrame(buildSendTextMsgFrame(contact.publicKey, outboundText));
|
||||||
buildSendTextMsgFrame(
|
|
||||||
contact.publicKey,
|
|
||||||
outboundText,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1052,13 +1066,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
) async {
|
) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
await sendFrame(buildUpdateContactPathFrame(
|
await sendFrame(
|
||||||
contact.publicKey,
|
buildUpdateContactPathFrame(
|
||||||
customPath,
|
contact.publicKey,
|
||||||
pathLen,
|
customPath,
|
||||||
type: contact.type,
|
pathLen,
|
||||||
name: contact.name,
|
type: contact.type,
|
||||||
));
|
name: contact.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set path override for a contact (persists across contact refreshes)
|
/// Set path override for a contact (persists across contact refreshes)
|
||||||
@@ -1068,16 +1084,27 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
int? pathLen,
|
int? pathLen,
|
||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
}) async {
|
}) async {
|
||||||
appLogger.info('setPathOverride called for ${contact.name}: pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}', tag: 'Connector');
|
appLogger.info(
|
||||||
|
'setPathOverride called for ${contact.name}: pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
// Find contact in list
|
// Find contact in list
|
||||||
final index = _contacts.indexWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
final index = _contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
|
);
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
appLogger.warn('setPathOverride: Contact not found in list: ${contact.name}', tag: 'Connector');
|
appLogger.warn(
|
||||||
|
'setPathOverride: Contact not found in list: ${contact.name}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.info('Found contact at index $index. Current override: ${_contacts[index].pathOverride}', tag: 'Connector');
|
appLogger.info(
|
||||||
|
'Found contact at index $index. Current override: ${_contacts[index].pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
// Update contact with new path override
|
// Update contact with new path override
|
||||||
_contacts[index] = _contacts[index].copyWith(
|
_contacts[index] = _contacts[index].copyWith(
|
||||||
@@ -1086,7 +1113,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
clearPathOverride: pathLen == null, // Clear if pathLen is null
|
clearPathOverride: pathLen == null, // Clear if pathLen is null
|
||||||
);
|
);
|
||||||
|
|
||||||
appLogger.info('Updated contact. New override: ${_contacts[index].pathOverride}, bytesLen: ${_contacts[index].pathOverrideBytes?.length}', tag: 'Connector');
|
appLogger.info(
|
||||||
|
'Updated contact. New override: ${_contacts[index].pathOverride}, bytesLen: ${_contacts[index].pathOverrideBytes?.length}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
// Save to storage
|
// Save to storage
|
||||||
await _contactStore.saveContacts(_contacts);
|
await _contactStore.saveContacts(_contacts);
|
||||||
@@ -1099,7 +1129,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
appLogger.info('Path sent to device', tag: 'Connector');
|
appLogger.info('Path sent to device', tag: 'Connector');
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Set path override for ${contact.name}: pathLen=$pathLen, bytes=${pathBytes?.length ?? 0}');
|
debugPrint(
|
||||||
|
'Set path override for ${contact.name}: pathLen=$pathLen, bytes=${pathBytes?.length ?? 0}',
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1148,7 +1180,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
outboundText,
|
outboundText,
|
||||||
selfKey,
|
selfKey,
|
||||||
);
|
);
|
||||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
final ackHashHex = ackHash
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
final messageBytes = utf8.encode(outboundText).length;
|
final messageBytes = utf8.encode(outboundText).length;
|
||||||
_pendingRepeaterAcks[ackHashHex]?.timeout?.cancel();
|
_pendingRepeaterAcks[ackHashHex]?.timeout?.cancel();
|
||||||
_pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext(
|
_pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext(
|
||||||
@@ -1206,7 +1240,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendChannelMessage(Channel channel, String text) async{
|
Future<void> sendChannelMessage(Channel channel, String text) async {
|
||||||
if (!isConnected || text.isEmpty) return;
|
if (!isConnected || text.isEmpty) return;
|
||||||
|
|
||||||
// Check if this is a reaction - if so, process it immediately instead of adding as a message
|
// Check if this is a reaction - if so, process it immediately instead of adding as a message
|
||||||
@@ -1215,9 +1249,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Check if we've already processed this reaction
|
// Check if we've already processed this reaction
|
||||||
_processedChannelReactions.putIfAbsent(channel.index, () => {});
|
_processedChannelReactions.putIfAbsent(channel.index, () => {});
|
||||||
final reactionKey = reactionInfo.reactionKey;
|
final reactionKey = reactionInfo.reactionKey;
|
||||||
final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null;
|
final reactionIdentifier = reactionKey != null
|
||||||
|
? '${reactionKey}_${reactionInfo.emoji}'
|
||||||
|
: null;
|
||||||
|
|
||||||
if (reactionIdentifier != null && _processedChannelReactions[channel.index]!.contains(reactionIdentifier)) {
|
if (reactionIdentifier != null &&
|
||||||
|
_processedChannelReactions[channel.index]!.contains(
|
||||||
|
reactionIdentifier,
|
||||||
|
)) {
|
||||||
// Already processed, don't process again
|
// Already processed, don't process again
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1242,13 +1281,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final message = ChannelMessage.outgoing(text, _selfName ?? 'Me', channel.index);
|
final message = ChannelMessage.outgoing(
|
||||||
|
text,
|
||||||
|
_selfName ?? 'Me',
|
||||||
|
channel.index,
|
||||||
|
);
|
||||||
_addChannelMessage(channel.index, message);
|
_addChannelMessage(channel.index, message);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
final isStructuredPayload = trimmed.startsWith('g:') || trimmed.startsWith('m:');
|
final isStructuredPayload =
|
||||||
final outboundText = (isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
trimmed.startsWith('g:') || trimmed.startsWith('m:');
|
||||||
|
final outboundText =
|
||||||
|
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||||
? Smaz.encodeIfSmaller(text)
|
? Smaz.encodeIfSmaller(text)
|
||||||
: text;
|
: text;
|
||||||
await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText));
|
await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText));
|
||||||
@@ -1264,9 +1309,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_conversations.remove(contact.publicKeyHex);
|
_conversations.remove(contact.publicKeyHex);
|
||||||
_loadedConversationKeys.remove(contact.publicKeyHex);
|
_loadedConversationKeys.remove(contact.publicKeyHex);
|
||||||
_contactLastReadMs.remove(contact.publicKeyHex);
|
_contactLastReadMs.remove(contact.publicKeyHex);
|
||||||
_unreadStore.saveContactLastRead(
|
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
|
||||||
Map<String, int>.from(_contactLastReadMs),
|
|
||||||
);
|
|
||||||
_messageStore.clearMessages(contact.publicKeyHex);
|
_messageStore.clearMessages(contact.publicKeyHex);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -1275,8 +1318,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
await sendFrame(buildResetPathFrame(contact.publicKey));
|
await sendFrame(buildResetPathFrame(contact.publicKey));
|
||||||
final existingIndex =
|
final existingIndex = _contacts.indexWhere(
|
||||||
_contacts.indexWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
|
);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
final existing = _contacts[existingIndex];
|
final existing = _contacts[existingIndex];
|
||||||
// Use copyWith to preserve pathOverride and pathOverrideBytes
|
// Use copyWith to preserve pathOverride and pathOverrideBytes
|
||||||
@@ -1295,8 +1339,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
int? pathLength,
|
int? pathLength,
|
||||||
}) {
|
}) {
|
||||||
final existingIndex =
|
final existingIndex = _contacts.indexWhere(
|
||||||
_contacts.indexWhere((c) => c.publicKeyHex == publicKeyHex);
|
(c) => c.publicKeyHex == publicKeyHex,
|
||||||
|
);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
final existing = _contacts[existingIndex];
|
final existing = _contacts[existingIndex];
|
||||||
_contacts[existingIndex] = existing.copyWith(
|
_contacts[existingIndex] = existing.copyWith(
|
||||||
@@ -1344,7 +1389,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_handleQueueSyncTimeout();
|
_handleQueueSyncTimeout();
|
||||||
});
|
});
|
||||||
|
|
||||||
debugPrint('[QueueSync] Requesting next message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)');
|
debugPrint(
|
||||||
|
'[QueueSync] Requesting next message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)',
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendFrame(buildSyncNextMessageFrame());
|
await sendFrame(buildSyncNextMessageFrame());
|
||||||
@@ -1358,7 +1405,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleQueueSyncTimeout() {
|
void _handleQueueSyncTimeout() {
|
||||||
debugPrint('[QueueSync] Timeout waiting for message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)');
|
debugPrint(
|
||||||
|
'[QueueSync] Timeout waiting for message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)',
|
||||||
|
);
|
||||||
|
|
||||||
if (_queueSyncRetries < _maxQueueSyncRetries) {
|
if (_queueSyncRetries < _maxQueueSyncRetries) {
|
||||||
// Retry
|
// Retry
|
||||||
@@ -1389,11 +1438,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
await sendFrame(buildSetAdvertNameFrame(name));
|
await sendFrame(buildSetAdvertNameFrame(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setNodeLocation({required double lat, required double lon}) async {
|
Future<void> setNodeLocation({
|
||||||
|
required double lat,
|
||||||
|
required double lon,
|
||||||
|
}) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
await sendFrame(buildSetAdvertLatLonFrame(lat, lon));
|
await sendFrame(buildSetAdvertLatLonFrame(lat, lon));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setCustomVar(String value) async {
|
||||||
|
if (!isConnected) return;
|
||||||
|
await sendFrame(buildSetCustomVarFrame(value));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> sendSelfAdvert({bool flood = true}) async {
|
Future<void> sendSelfAdvert({bool flood = true}) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
await sendFrame(buildSendSelfAdvertFrame(flood: flood));
|
await sendFrame(buildSendSelfAdvertFrame(flood: flood));
|
||||||
@@ -1424,7 +1481,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_channelSyncRetries = 0;
|
_channelSyncRetries = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
debugPrint('[ChannelSync] Starting sync for $_totalChannelsToRequest channels');
|
debugPrint(
|
||||||
|
'[ChannelSync] Starting sync for $_totalChannelsToRequest channels',
|
||||||
|
);
|
||||||
|
|
||||||
// Start sequential sync
|
// Start sequential sync
|
||||||
await _requestNextChannel();
|
await _requestNextChannel();
|
||||||
@@ -1456,7 +1515,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
() => _handleChannelSyncTimeout(channelIndex),
|
() => _handleChannelSyncTimeout(channelIndex),
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('[ChannelSync] Requesting channel $channelIndex/$_totalChannelsToRequest (retry: $_channelSyncRetries/$_maxChannelSyncRetries)');
|
debugPrint(
|
||||||
|
'[ChannelSync] Requesting channel $channelIndex/$_totalChannelsToRequest (retry: $_channelSyncRetries/$_maxChannelSyncRetries)',
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendFrame(buildGetChannelFrame(channelIndex));
|
await sendFrame(buildGetChannelFrame(channelIndex));
|
||||||
@@ -1468,7 +1529,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleChannelSyncTimeout(int channelIndex) {
|
void _handleChannelSyncTimeout(int channelIndex) {
|
||||||
debugPrint('[ChannelSync] Timeout waiting for channel $channelIndex (retry: $_channelSyncRetries/$_maxChannelSyncRetries)');
|
debugPrint(
|
||||||
|
'[ChannelSync] Timeout waiting for channel $channelIndex (retry: $_channelSyncRetries/$_maxChannelSyncRetries)',
|
||||||
|
);
|
||||||
|
|
||||||
if (_channelSyncRetries < _maxChannelSyncRetries) {
|
if (_channelSyncRetries < _maxChannelSyncRetries) {
|
||||||
// Retry the same channel
|
// Retry the same channel
|
||||||
@@ -1477,16 +1540,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
unawaited(_requestNextChannel());
|
unawaited(_requestNextChannel());
|
||||||
} else {
|
} else {
|
||||||
// Max retries reached for this channel, restore from cache and move to next
|
// Max retries reached for this channel, restore from cache and move to next
|
||||||
debugPrint('[ChannelSync] Max retries reached for channel $channelIndex, attempting cache restore');
|
debugPrint(
|
||||||
|
'[ChannelSync] Max retries reached for channel $channelIndex, attempting cache restore',
|
||||||
|
);
|
||||||
|
|
||||||
// Try to restore this channel from cache
|
// Try to restore this channel from cache
|
||||||
try {
|
try {
|
||||||
final cachedChannel = _previousChannelsCache.firstWhere(
|
final cachedChannel = _previousChannelsCache.firstWhere(
|
||||||
(c) => c.index == channelIndex
|
(c) => c.index == channelIndex,
|
||||||
);
|
);
|
||||||
if (!cachedChannel.isEmpty) {
|
if (!cachedChannel.isEmpty) {
|
||||||
_channels.add(cachedChannel);
|
_channels.add(cachedChannel);
|
||||||
debugPrint('[ChannelSync] Restored channel $channelIndex (${cachedChannel.name}) from cache');
|
debugPrint(
|
||||||
|
'[ChannelSync] Restored channel $channelIndex (${cachedChannel.name}) from cache',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No cached channel found, that's okay
|
// No cached channel found, that's okay
|
||||||
@@ -1503,7 +1570,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
void _completeChannelSync() {
|
void _completeChannelSync() {
|
||||||
_channelSyncTimeout?.cancel();
|
_channelSyncTimeout?.cancel();
|
||||||
|
|
||||||
debugPrint('[ChannelSync] Sync complete: received ${_channels.length}/$_totalChannelsToRequest channels');
|
debugPrint(
|
||||||
|
'[ChannelSync] Sync complete: received ${_channels.length}/$_totalChannelsToRequest channels',
|
||||||
|
);
|
||||||
|
|
||||||
_cleanupChannelSync(completed: true);
|
_cleanupChannelSync(completed: true);
|
||||||
|
|
||||||
@@ -1541,9 +1610,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Delete by setting empty name and zero PSK
|
// Delete by setting empty name and zero PSK
|
||||||
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
|
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
|
||||||
_channelLastReadMs.remove(index);
|
_channelLastReadMs.remove(index);
|
||||||
_unreadStore.saveChannelLastRead(
|
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
|
||||||
Map<int, int>.from(_channelLastReadMs),
|
|
||||||
);
|
|
||||||
// Refresh channels after deleting
|
// Refresh channels after deleting
|
||||||
await getChannels();
|
await getChannels();
|
||||||
}
|
}
|
||||||
@@ -1705,8 +1772,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
||||||
final reportedContacts = frame[2];
|
final reportedContacts = frame[2];
|
||||||
final reportedChannels = frame[3];
|
final reportedChannels = frame[3];
|
||||||
final nextMaxContacts = reportedContacts > 0 ? reportedContacts * 2 : _maxContacts;
|
final nextMaxContacts = reportedContacts > 0
|
||||||
final nextMaxChannels = reportedChannels > 0 ? reportedChannels : _maxChannels;
|
? reportedContacts * 2
|
||||||
|
: _maxContacts;
|
||||||
|
final nextMaxChannels = reportedChannels > 0
|
||||||
|
? reportedChannels
|
||||||
|
: _maxChannels;
|
||||||
final previousMaxChannels = _maxChannels;
|
final previousMaxChannels = _maxChannels;
|
||||||
if (nextMaxContacts != _maxContacts || nextMaxChannels != _maxChannels) {
|
if (nextMaxContacts != _maxContacts || nextMaxChannels != _maxChannels) {
|
||||||
_maxContacts = nextMaxContacts;
|
_maxContacts = nextMaxContacts;
|
||||||
@@ -1751,7 +1822,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_currentBwHz = readUint32LE(frame, 5);
|
_currentBwHz = readUint32LE(frame, 5);
|
||||||
_currentSf = frame[9];
|
_currentSf = frame[9];
|
||||||
_currentCr = frame[10];
|
_currentCr = frame[10];
|
||||||
debugPrint('Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr');
|
debugPrint(
|
||||||
|
'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr',
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1822,11 +1895,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
final existing = _contacts[existingIndex];
|
final existing = _contacts[existingIndex];
|
||||||
final mergedLastMessageAt = existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
final mergedLastMessageAt =
|
||||||
|
existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
||||||
? existing.lastMessageAt
|
? existing.lastMessageAt
|
||||||
: contact.lastMessageAt;
|
: contact.lastMessageAt;
|
||||||
|
|
||||||
appLogger.info('Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}', tag: 'Connector');
|
appLogger.info(
|
||||||
|
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||||
_contacts[existingIndex] = contact.copyWith(
|
_contacts[existingIndex] = contact.copyWith(
|
||||||
@@ -1835,10 +1912,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
pathOverrideBytes: existing.pathOverrideBytes,
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
);
|
);
|
||||||
|
|
||||||
appLogger.info('After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', tag: 'Connector');
|
appLogger.info(
|
||||||
|
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
_contacts.add(contact);
|
_contacts.add(contact);
|
||||||
appLogger.info('Added new contact ${contact.name}: pathLen=${contact.pathLength}', tag: 'Connector');
|
appLogger.info(
|
||||||
|
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_knownContactKeys.add(contact.publicKeyHex);
|
_knownContactKeys.add(contact.publicKeyHex);
|
||||||
_loadMessagesForContact(contact.publicKeyHex);
|
_loadMessagesForContact(contact.publicKeyHex);
|
||||||
@@ -1970,9 +2053,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (message == null && !_isLoadingContacts) {
|
if (message == null && !_isLoadingContacts) {
|
||||||
final senderPrefix = _extractSenderPrefix(frame);
|
final senderPrefix = _extractSenderPrefix(frame);
|
||||||
if (senderPrefix != null) {
|
if (senderPrefix != null) {
|
||||||
final hasContact = _contacts.any((c) => _matchesPrefix(c.publicKey, senderPrefix));
|
final hasContact = _contacts.any(
|
||||||
|
(c) => _matchesPrefix(c.publicKey, senderPrefix),
|
||||||
|
);
|
||||||
if (!hasContact) {
|
if (!hasContact) {
|
||||||
debugPrint('Received message from unknown contact, refreshing contacts...');
|
debugPrint(
|
||||||
|
'Received message from unknown contact, refreshing contacts...',
|
||||||
|
);
|
||||||
await refreshContactsSinceLastmod();
|
await refreshContactsSinceLastmod();
|
||||||
// Retry parsing after refresh
|
// Retry parsing after refresh
|
||||||
message = _parseContactMessage(frame);
|
message = _parseContactMessage(frame);
|
||||||
@@ -2017,7 +2104,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// Show notification for new incoming message
|
// Show notification for new incoming message
|
||||||
if (!message.isOutgoing && !message.isCli && _appSettingsService != null) {
|
if (!message.isOutgoing &&
|
||||||
|
!message.isCli &&
|
||||||
|
_appSettingsService != null) {
|
||||||
final settings = _appSettingsService!.settings;
|
final settings = _appSettingsService!.settings;
|
||||||
if (settings.notificationsEnabled && settings.notifyOnNewMessage) {
|
if (settings.notificationsEnabled && settings.notifyOnNewMessage) {
|
||||||
// Find the contact name
|
// Find the contact name
|
||||||
@@ -2074,9 +2163,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
// Try base text offset; if empty and there is room for the optional 4-byte extra
|
// Try base text offset; if empty and there is room for the optional 4-byte extra
|
||||||
// (used by signed/plain variants), try again skipping those bytes.
|
// (used by signed/plain variants), try again skipping those bytes.
|
||||||
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset);
|
var text = readCString(
|
||||||
|
frame,
|
||||||
|
baseTextOffset,
|
||||||
|
frame.length - baseTextOffset,
|
||||||
|
);
|
||||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
||||||
text = readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4));
|
text = readCString(
|
||||||
|
frame,
|
||||||
|
baseTextOffset + 4,
|
||||||
|
frame.length - (baseTextOffset + 4),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (text.isEmpty) return null;
|
if (text.isEmpty) return null;
|
||||||
final decodedText = isCli ? text : (Smaz.tryDecodePrefixed(text) ?? text);
|
final decodedText = isCli ? text : (Smaz.tryDecodePrefixed(text) ?? text);
|
||||||
@@ -2099,7 +2196,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
status: MessageStatus.delivered,
|
status: MessageStatus.delivered,
|
||||||
pathLength: pathLenByte == 0xFF ? 0 : pathLenByte,
|
pathLength: pathLenByte == 0xFF ? 0 : pathLenByte,
|
||||||
pathBytes: Uint8List(0),
|
pathBytes: Uint8List(0),
|
||||||
fourByteRoomContactKey: fourBytePubMSG
|
fourByteRoomContactKey: fourBytePubMSG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2140,17 +2237,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
String prepareContactOutboundText(Contact contact, String text) {
|
String prepareContactOutboundText(Contact contact, String text) {
|
||||||
final trimmed = text.trim();
|
final trimmed = text.trim();
|
||||||
final isStructuredPayload =
|
final isStructuredPayload =
|
||||||
trimmed.startsWith('g:') || trimmed.startsWith('m:') || trimmed.startsWith('V1|');
|
trimmed.startsWith('g:') ||
|
||||||
|
trimmed.startsWith('m:') ||
|
||||||
|
trimmed.startsWith('V1|');
|
||||||
if (!isStructuredPayload && isContactSmazEnabled(contact.publicKeyHex)) {
|
if (!isStructuredPayload && isContactSmazEnabled(contact.publicKeyHex)) {
|
||||||
return Smaz.encodeIfSmaller(text);
|
return Smaz.encodeIfSmaller(text);
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
String _channelDisplayName(int channelIndex) {
|
String _channelDisplayName(int channelIndex) {
|
||||||
for (final channel in _channels) {
|
for (final channel in _channels) {
|
||||||
if (channel.index != channelIndex) continue;
|
if (channel.index != channelIndex) continue;
|
||||||
@@ -2184,7 +2279,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
void _handleIncomingChannelMessage(Uint8List frame) {
|
void _handleIncomingChannelMessage(Uint8List frame) {
|
||||||
final message = ChannelMessage.fromFrame(frame);
|
final message = ChannelMessage.fromFrame(frame);
|
||||||
if (message != null && message.channelIndex != null) {
|
if (message != null && message.channelIndex != null) {
|
||||||
if (_shouldDropSelfChannelMessage(message.senderName, message.pathBytes)) {
|
if (_shouldDropSelfChannelMessage(
|
||||||
|
message.senderName,
|
||||||
|
message.pathBytes,
|
||||||
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_updateContactLastMessageAtByName(
|
_updateContactLastMessageAtByName(
|
||||||
@@ -2257,7 +2355,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_maybeMarkActiveChannelRead(message);
|
_maybeMarkActiveChannelRead(message);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
final label = channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name;
|
final label = channel.name.isEmpty
|
||||||
|
? 'Channel ${channel.index}'
|
||||||
|
: channel.name;
|
||||||
_maybeNotifyChannelMessage(message, channelName: label);
|
_maybeNotifyChannelMessage(message, channelName: label);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -2277,7 +2377,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
// Check if this is a CLI command ACK - if so, ignore it
|
// Check if this is a CLI command ACK - if so, ignore it
|
||||||
if (_lastSentWasCliCommand) {
|
if (_lastSentWasCliCommand) {
|
||||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
final ackHashHex = ackHash
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
debugPrint('Ignoring CLI command ACK (sent): $ackHashHex');
|
debugPrint('Ignoring CLI command ACK (sent): $ackHashHex');
|
||||||
_lastSentWasCliCommand = false;
|
_lastSentWasCliCommand = false;
|
||||||
return;
|
return;
|
||||||
@@ -2294,7 +2396,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Fallback to old behavior
|
// Fallback to old behavior
|
||||||
for (var messages in _conversations.values) {
|
for (var messages in _conversations.values) {
|
||||||
for (int i = messages.length - 1; i >= 0; i--) {
|
for (int i = messages.length - 1; i >= 0; i--) {
|
||||||
if (messages[i].isOutgoing && messages[i].status == MessageStatus.pending) {
|
if (messages[i].isOutgoing &&
|
||||||
|
messages[i].status == MessageStatus.pending) {
|
||||||
messages[i] = messages[i].copyWith(status: MessageStatus.sent);
|
messages[i] = messages[i].copyWith(status: MessageStatus.sent);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
@@ -2328,7 +2431,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Fallback to old behavior
|
// Fallback to old behavior
|
||||||
for (var messages in _conversations.values) {
|
for (var messages in _conversations.values) {
|
||||||
for (int i = messages.length - 1; i >= 0; i--) {
|
for (int i = messages.length - 1; i >= 0; i--) {
|
||||||
if (messages[i].isOutgoing && messages[i].status == MessageStatus.sent) {
|
if (messages[i].isOutgoing &&
|
||||||
|
messages[i].status == MessageStatus.sent) {
|
||||||
messages[i] = messages[i].copyWith(status: MessageStatus.delivered);
|
messages[i] = messages[i].copyWith(status: MessageStatus.delivered);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
@@ -2339,7 +2443,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) {
|
bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) {
|
||||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
final ackHashHex = ackHash
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
final entry = _pendingRepeaterAcks[ackHashHex];
|
final entry = _pendingRepeaterAcks[ackHashHex];
|
||||||
if (entry == null) return false;
|
if (entry == null) return false;
|
||||||
|
|
||||||
@@ -2358,7 +2464,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) {
|
bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) {
|
||||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
final ackHashHex = ackHash
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
final entry = _pendingRepeaterAcks.remove(ackHashHex);
|
final entry = _pendingRepeaterAcks.remove(ackHashHex);
|
||||||
if (entry == null) return false;
|
if (entry == null) return false;
|
||||||
entry.timeout?.cancel();
|
entry.timeout?.cancel();
|
||||||
@@ -2370,7 +2478,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
final channel = Channel.fromFrame(frame);
|
final channel = Channel.fromFrame(frame);
|
||||||
if (channel == null) return;
|
if (channel == null) return;
|
||||||
|
|
||||||
debugPrint('[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}');
|
debugPrint(
|
||||||
|
'[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}',
|
||||||
|
);
|
||||||
|
|
||||||
// If we're syncing and this is the channel we're waiting for
|
// If we're syncing and this is the channel we're waiting for
|
||||||
if (_isSyncingChannels && _channelSyncInFlight) {
|
if (_isSyncingChannels && _channelSyncInFlight) {
|
||||||
@@ -2392,9 +2502,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
// Received a channel but not the one we're waiting for
|
// Received a channel but not the one we're waiting for
|
||||||
// This can happen if device sends unsolicited updates
|
// This can happen if device sends unsolicited updates
|
||||||
debugPrint('[ChannelSync] Received unexpected channel ${channel.index}, expected $_nextChannelIndexToRequest');
|
debugPrint(
|
||||||
|
'[ChannelSync] Received unexpected channel ${channel.index}, expected $_nextChannelIndexToRequest',
|
||||||
|
);
|
||||||
// Add it anyway but don't advance sync
|
// Add it anyway but don't advance sync
|
||||||
if (!channel.isEmpty && !_channels.any((c) => c.index == channel.index)) {
|
if (!channel.isEmpty &&
|
||||||
|
!_channels.any((c) => c.index == channel.index)) {
|
||||||
_channels.add(channel);
|
_channels.add(channel);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -2404,7 +2517,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Not syncing, or received unsolicited update - handle normally
|
// Not syncing, or received unsolicited update - handle normally
|
||||||
if (!channel.isEmpty) {
|
if (!channel.isEmpty) {
|
||||||
// Update or add channel
|
// Update or add channel
|
||||||
final existingIndex = _channels.indexWhere((c) => c.index == channel.index);
|
final existingIndex = _channels.indexWhere(
|
||||||
|
(c) => c.index == channel.index,
|
||||||
|
);
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
_channels[existingIndex] = channel;
|
_channels[existingIndex] = channel;
|
||||||
} else {
|
} else {
|
||||||
@@ -2469,26 +2584,30 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return latestMs;
|
return latestMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setContactLastReadMs(String contactKeyHex, int timestampMs, {bool notify = true}) {
|
void _setContactLastReadMs(
|
||||||
|
String contactKeyHex,
|
||||||
|
int timestampMs, {
|
||||||
|
bool notify = true,
|
||||||
|
}) {
|
||||||
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
|
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
|
||||||
final existing = _contactLastReadMs[contactKeyHex] ?? 0;
|
final existing = _contactLastReadMs[contactKeyHex] ?? 0;
|
||||||
if (timestampMs <= existing) return;
|
if (timestampMs <= existing) return;
|
||||||
_contactLastReadMs[contactKeyHex] = timestampMs;
|
_contactLastReadMs[contactKeyHex] = timestampMs;
|
||||||
_unreadStore.saveContactLastRead(
|
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
|
||||||
Map<String, int>.from(_contactLastReadMs),
|
|
||||||
);
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setChannelLastReadMs(int channelIndex, int timestampMs, {bool notify = true}) {
|
void _setChannelLastReadMs(
|
||||||
|
int channelIndex,
|
||||||
|
int timestampMs, {
|
||||||
|
bool notify = true,
|
||||||
|
}) {
|
||||||
final existing = _channelLastReadMs[channelIndex] ?? 0;
|
final existing = _channelLastReadMs[channelIndex] ?? 0;
|
||||||
if (timestampMs <= existing) return;
|
if (timestampMs <= existing) return;
|
||||||
_channelLastReadMs[channelIndex] = timestampMs;
|
_channelLastReadMs[channelIndex] = timestampMs;
|
||||||
_unreadStore.saveChannelLastRead(
|
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
|
||||||
Map<int, int>.from(_channelLastReadMs),
|
|
||||||
);
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -2526,9 +2645,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Check if we've already processed this exact reaction using lightweight key
|
// Check if we've already processed this exact reaction using lightweight key
|
||||||
_processedContactReactions.putIfAbsent(pubKeyHex, () => {});
|
_processedContactReactions.putIfAbsent(pubKeyHex, () => {});
|
||||||
final reactionKey = reactionInfo.reactionKey;
|
final reactionKey = reactionInfo.reactionKey;
|
||||||
final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null;
|
final reactionIdentifier = reactionKey != null
|
||||||
|
? '${reactionKey}_${reactionInfo.emoji}'
|
||||||
|
: null;
|
||||||
|
|
||||||
final isDuplicate = reactionIdentifier != null &&
|
final isDuplicate =
|
||||||
|
reactionIdentifier != null &&
|
||||||
_processedContactReactions[pubKeyHex]!.contains(reactionIdentifier);
|
_processedContactReactions[pubKeyHex]!.contains(reactionIdentifier);
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
@@ -2551,7 +2673,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _processContactReaction(List<Message> messages, ReactionInfo reactionInfo) {
|
void _processContactReaction(
|
||||||
|
List<Message> messages,
|
||||||
|
ReactionInfo reactionInfo,
|
||||||
|
) {
|
||||||
// Find target message by messageId
|
// Find target message by messageId
|
||||||
for (int i = 0; i < messages.length; i++) {
|
for (int i = 0; i < messages.length; i++) {
|
||||||
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
||||||
@@ -2570,7 +2695,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
final header = raw[index++];
|
final header = raw[index++];
|
||||||
final routeType = header & _phRouteMask;
|
final routeType = header & _phRouteMask;
|
||||||
final hasTransport = routeType == _routeTransportFlood || routeType == _routeTransportDirect;
|
final hasTransport =
|
||||||
|
routeType == _routeTransportFlood || routeType == _routeTransportDirect;
|
||||||
if (hasTransport) {
|
if (hasTransport) {
|
||||||
if (raw.length < index + 4) return null;
|
if (raw.length < index + 4) return null;
|
||||||
index += 4;
|
index += 4;
|
||||||
@@ -2633,7 +2759,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
if (RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||||
return _ParsedText(senderName: 'Unknown', text: text);
|
return _ParsedText(senderName: 'Unknown', text: text);
|
||||||
}
|
}
|
||||||
final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
final offset =
|
||||||
|
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||||
? colonIndex + 2
|
? colonIndex + 2
|
||||||
: colonIndex + 1;
|
: colonIndex + 1;
|
||||||
return _ParsedText(
|
return _ParsedText(
|
||||||
@@ -2670,10 +2797,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return contact.path;
|
return contact.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _resolveOutgoingPathLength(
|
int? _resolveOutgoingPathLength(Contact contact, PathSelection? selection) {
|
||||||
Contact contact,
|
|
||||||
PathSelection? selection,
|
|
||||||
) {
|
|
||||||
// Priority 1: Check user's path override
|
// Priority 1: Check user's path override
|
||||||
if (contact.pathOverride != null) {
|
if (contact.pathOverride != null) {
|
||||||
return contact.pathOverride;
|
return contact.pathOverride;
|
||||||
@@ -2714,10 +2838,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
// Check if we've already processed this exact reaction using lightweight key
|
// Check if we've already processed this exact reaction using lightweight key
|
||||||
_processedChannelReactions.putIfAbsent(channelIndex, () => {});
|
_processedChannelReactions.putIfAbsent(channelIndex, () => {});
|
||||||
final reactionKey = reactionInfo.reactionKey;
|
final reactionKey = reactionInfo.reactionKey;
|
||||||
final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null;
|
final reactionIdentifier = reactionKey != null
|
||||||
|
? '${reactionKey}_${reactionInfo.emoji}'
|
||||||
|
: null;
|
||||||
|
|
||||||
final isDuplicate = reactionIdentifier != null &&
|
final isDuplicate =
|
||||||
_processedChannelReactions[channelIndex]!.contains(reactionIdentifier);
|
reactionIdentifier != null &&
|
||||||
|
_processedChannelReactions[channelIndex]!.contains(
|
||||||
|
reactionIdentifier,
|
||||||
|
);
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
// New reaction - process it
|
// New reaction - process it
|
||||||
@@ -2739,7 +2868,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
if (replyInfo != null) {
|
if (replyInfo != null) {
|
||||||
// Find original message by sender name (most recent match)
|
// Find original message by sender name (most recent match)
|
||||||
final originalMessage = _findMessageBySender(messages, replyInfo.mentionedNode);
|
final originalMessage = _findMessageBySender(
|
||||||
|
messages,
|
||||||
|
replyInfo.mentionedNode,
|
||||||
|
);
|
||||||
|
|
||||||
if (originalMessage != null) {
|
if (originalMessage != null) {
|
||||||
// Create new message with reply metadata
|
// Create new message with reply metadata
|
||||||
@@ -2769,8 +2901,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
isNew = false;
|
isNew = false;
|
||||||
final existing = messages[existingIndex];
|
final existing = messages[existingIndex];
|
||||||
final mergedPathBytes = _selectPreferredPathBytes(existing.pathBytes, processedMessage.pathBytes);
|
final mergedPathBytes = _selectPreferredPathBytes(
|
||||||
final mergedPathVariants = _mergePathVariants(existing.pathVariants, processedMessage.pathVariants);
|
existing.pathBytes,
|
||||||
|
processedMessage.pathBytes,
|
||||||
|
);
|
||||||
|
final mergedPathVariants = _mergePathVariants(
|
||||||
|
existing.pathVariants,
|
||||||
|
processedMessage.pathVariants,
|
||||||
|
);
|
||||||
final mergedPathLength = _mergePathLength(
|
final mergedPathLength = _mergePathLength(
|
||||||
existing.pathLength,
|
existing.pathLength,
|
||||||
processedMessage.pathLength,
|
processedMessage.pathLength,
|
||||||
@@ -2783,7 +2921,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
pathBytes: mergedPathBytes,
|
pathBytes: mergedPathBytes,
|
||||||
pathVariants: mergedPathVariants,
|
pathVariants: mergedPathVariants,
|
||||||
// Mark as sent when first repeat is heard
|
// Mark as sent when first repeat is heard
|
||||||
status: newRepeatCount == 1 && existing.status == ChannelMessageStatus.pending
|
status:
|
||||||
|
newRepeatCount == 1 &&
|
||||||
|
existing.status == ChannelMessageStatus.pending
|
||||||
? ChannelMessageStatus.sent
|
? ChannelMessageStatus.sent
|
||||||
: existing.status,
|
: existing.status,
|
||||||
);
|
);
|
||||||
@@ -2792,14 +2932,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to persistent storage
|
// Save to persistent storage
|
||||||
_channelMessageStore.saveChannelMessages(
|
_channelMessageStore.saveChannelMessages(channelIndex, messages);
|
||||||
channelIndex,
|
|
||||||
messages,
|
|
||||||
);
|
|
||||||
return isNew;
|
return isNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelMessage? _findMessageBySender(List<ChannelMessage> messages, String mentionedNode) {
|
ChannelMessage? _findMessageBySender(
|
||||||
|
List<ChannelMessage> messages,
|
||||||
|
String mentionedNode,
|
||||||
|
) {
|
||||||
// Search backwards for most recent message from this sender
|
// Search backwards for most recent message from this sender
|
||||||
for (int i = messages.length - 1; i >= 0; i--) {
|
for (int i = messages.length - 1; i >= 0; i--) {
|
||||||
if (messages[i].senderName == mentionedNode && !messages[i].isOutgoing) {
|
if (messages[i].senderName == mentionedNode && !messages[i].isOutgoing) {
|
||||||
@@ -2809,7 +2949,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _processReaction(List<ChannelMessage> messages, ReactionInfo reactionInfo) {
|
void _processReaction(
|
||||||
|
List<ChannelMessage> messages,
|
||||||
|
ReactionInfo reactionInfo,
|
||||||
|
) {
|
||||||
// Find target message by messageId
|
// Find target message by messageId
|
||||||
for (int i = 0; i < messages.length; i++) {
|
for (int i = 0; i < messages.length; i++) {
|
||||||
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
||||||
@@ -2824,7 +2967,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int _findChannelRepeatIndex(List<ChannelMessage> messages, ChannelMessage incoming) {
|
int _findChannelRepeatIndex(
|
||||||
|
List<ChannelMessage> messages,
|
||||||
|
ChannelMessage incoming,
|
||||||
|
) {
|
||||||
for (int i = messages.length - 1; i >= 0; i--) {
|
for (int i = messages.length - 1; i >= 0; i--) {
|
||||||
final existing = messages[i];
|
final existing = messages[i];
|
||||||
if (_isChannelRepeat(existing, incoming)) {
|
if (_isChannelRepeat(existing, incoming)) {
|
||||||
@@ -2837,9 +2983,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) {
|
bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) {
|
||||||
if (existing.text != incoming.text) return false;
|
if (existing.text != incoming.text) return false;
|
||||||
|
|
||||||
final diffMs = (existing.timestamp.millisecondsSinceEpoch -
|
final diffMs =
|
||||||
incoming.timestamp.millisecondsSinceEpoch)
|
(existing.timestamp.millisecondsSinceEpoch -
|
||||||
.abs();
|
incoming.timestamp.millisecondsSinceEpoch)
|
||||||
|
.abs();
|
||||||
if (diffMs > 5000) return false;
|
if (diffMs > 5000) return false;
|
||||||
|
|
||||||
if (existing.senderName == incoming.senderName) return true;
|
if (existing.senderName == incoming.senderName) return true;
|
||||||
@@ -3013,17 +3160,15 @@ class _RawPacket {
|
|||||||
required this.payload,
|
required this.payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isFlood => routeType == _routeFlood || routeType == _routeTransportFlood;
|
bool get isFlood =>
|
||||||
|
routeType == _routeFlood || routeType == _routeTransportFlood;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ParsedText {
|
class _ParsedText {
|
||||||
final String senderName;
|
final String senderName;
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
_ParsedText({
|
_ParsedText({required this.senderName, required this.text});
|
||||||
required this.senderName,
|
|
||||||
required this.text,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RepeaterAckContext {
|
class _RepeaterAckContext {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class BufferReader {
|
|||||||
|
|
||||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||||
|
|
||||||
String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true);
|
String readString() =>
|
||||||
|
utf8.decode(readRemainingBytes(), allowMalformed: true);
|
||||||
|
|
||||||
String readCString(int maxLength) {
|
String readCString(int maxLength) {
|
||||||
final value = <int>[];
|
final value = <int>[];
|
||||||
@@ -38,13 +39,19 @@ class BufferReader {
|
|||||||
|
|
||||||
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
||||||
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
||||||
int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
|
int readUInt16LE() =>
|
||||||
int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
|
readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
|
||||||
int readUInt32LE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
|
int readUInt16BE() =>
|
||||||
int readUInt32BE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
|
readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
|
||||||
int readInt16LE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
|
int readUInt32LE() =>
|
||||||
|
readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
|
||||||
|
int readUInt32BE() =>
|
||||||
|
readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
|
||||||
|
int readInt16LE() =>
|
||||||
|
readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
|
||||||
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
|
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
|
||||||
int readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
|
int readInt32LE() =>
|
||||||
|
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
|
||||||
|
|
||||||
int readInt24BE() {
|
int readInt24BE() {
|
||||||
var value = (readByte() << 16) | (readByte() << 8) | readByte();
|
var value = (readByte() << 16) | (readByte() << 8) | readByte();
|
||||||
@@ -63,21 +70,25 @@ class BufferWriter {
|
|||||||
void writeBytes(Uint8List bytes) => _builder.add(bytes);
|
void writeBytes(Uint8List bytes) => _builder.add(bytes);
|
||||||
|
|
||||||
void writeUInt16LE(int num) {
|
void writeUInt16LE(int num) {
|
||||||
final bytes = Uint8List(2)..buffer.asByteData().setUint16(0, num, Endian.little);
|
final bytes = Uint8List(2)
|
||||||
|
..buffer.asByteData().setUint16(0, num, Endian.little);
|
||||||
writeBytes(bytes);
|
writeBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeUInt32LE(int num) {
|
void writeUInt32LE(int num) {
|
||||||
final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, num, Endian.little);
|
final bytes = Uint8List(4)
|
||||||
|
..buffer.asByteData().setUint32(0, num, Endian.little);
|
||||||
writeBytes(bytes);
|
writeBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeInt32LE(int num) {
|
void writeInt32LE(int num) {
|
||||||
final bytes = Uint8List(4)..buffer.asByteData().setInt32(0, num, Endian.little);
|
final bytes = Uint8List(4)
|
||||||
|
..buffer.asByteData().setInt32(0, num, Endian.little);
|
||||||
writeBytes(bytes);
|
writeBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeString(String string) => writeBytes(Uint8List.fromList(utf8.encode(string)));
|
void writeString(String string) =>
|
||||||
|
writeBytes(Uint8List.fromList(utf8.encode(string)));
|
||||||
|
|
||||||
void writeCString(String string, int maxLength) {
|
void writeCString(String string, int maxLength) {
|
||||||
final bytes = Uint8List(maxLength);
|
final bytes = Uint8List(maxLength);
|
||||||
@@ -118,6 +129,7 @@ const int cmdGetChannel = 31;
|
|||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdGetRadioSettings = 57;
|
||||||
const int cmdGetTelemetryReq = 39;
|
const int cmdGetTelemetryReq = 39;
|
||||||
|
const int cmdSetCustomVar = 41;
|
||||||
const int cmdSendBinaryReq = 50;
|
const int cmdSendBinaryReq = 50;
|
||||||
|
|
||||||
// Text message types
|
// Text message types
|
||||||
@@ -166,7 +178,6 @@ const int pushCodeNewAdvert = 0x8A;
|
|||||||
const int pushCodeTelemetryResponse = 0x8B;
|
const int pushCodeTelemetryResponse = 0x8B;
|
||||||
const int pushCodeBinaryResponse = 0x8C;
|
const int pushCodeBinaryResponse = 0x8C;
|
||||||
|
|
||||||
|
|
||||||
// Contact/advertisement types
|
// Contact/advertisement types
|
||||||
const int advTypeChat = 1;
|
const int advTypeChat = 1;
|
||||||
const int advTypeRepeater = 2;
|
const int advTypeRepeater = 2;
|
||||||
@@ -233,10 +244,7 @@ class ParsedContactText {
|
|||||||
final Uint8List senderPrefix;
|
final Uint8List senderPrefix;
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
const ParsedContactText({
|
const ParsedContactText({required this.senderPrefix, required this.text});
|
||||||
required this.senderPrefix,
|
|
||||||
required this.text,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
||||||
@@ -265,10 +273,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset).trim();
|
var text = readCString(
|
||||||
|
frame,
|
||||||
|
baseTextOffset,
|
||||||
|
frame.length - baseTextOffset,
|
||||||
|
).trim();
|
||||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
||||||
text =
|
text = readCString(
|
||||||
readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)).trim();
|
frame,
|
||||||
|
baseTextOffset + 4,
|
||||||
|
frame.length - (baseTextOffset + 4),
|
||||||
|
).trim();
|
||||||
}
|
}
|
||||||
if (text.isEmpty) return null;
|
if (text.isEmpty) return null;
|
||||||
|
|
||||||
@@ -362,7 +377,8 @@ Uint8List buildSendTextMsgFrame(
|
|||||||
int attempt = 0,
|
int attempt = 0,
|
||||||
int? timestampSeconds,
|
int? timestampSeconds,
|
||||||
}) {
|
}) {
|
||||||
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
final timestamp =
|
||||||
|
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSendTxtMsg);
|
writer.writeByte(cmdSendTxtMsg);
|
||||||
writer.writeByte(txtTypePlain);
|
writer.writeByte(txtTypePlain);
|
||||||
@@ -444,7 +460,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
|
|||||||
// Format: [cmd][name...]
|
// Format: [cmd][name...]
|
||||||
Uint8List buildSetAdvertNameFrame(String name) {
|
Uint8List buildSetAdvertNameFrame(String name) {
|
||||||
final nameBytes = utf8.encode(name);
|
final nameBytes = utf8.encode(name);
|
||||||
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1;
|
final nameLen = nameBytes.length < maxNameSize
|
||||||
|
? nameBytes.length
|
||||||
|
: maxNameSize - 1;
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSetAdvertName);
|
writer.writeByte(cmdSetAdvertName);
|
||||||
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
|
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
|
||||||
@@ -461,6 +479,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
|
|||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint8List buildSetCustomVarFrame(String value) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetCustomVar);
|
||||||
|
writer.writeString(value);
|
||||||
|
writer.writeByte(0);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
// Build CMD_REBOOT frame
|
// Build CMD_REBOOT frame
|
||||||
// Format: [cmd]["reboot"]
|
// Format: [cmd]["reboot"]
|
||||||
Uint8List buildRebootFrame() {
|
Uint8List buildRebootFrame() {
|
||||||
@@ -544,7 +570,9 @@ Uint8List buildUpdateContactPathFrame(
|
|||||||
// Path data (64 bytes, zero-padded)
|
// Path data (64 bytes, zero-padded)
|
||||||
final pathPadded = Uint8List(maxPathSize);
|
final pathPadded = Uint8List(maxPathSize);
|
||||||
if (customPath.isNotEmpty && pathLen > 0) {
|
if (customPath.isNotEmpty && pathLen > 0) {
|
||||||
final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize;
|
final copyLen = customPath.length < maxPathSize
|
||||||
|
? customPath.length
|
||||||
|
: maxPathSize;
|
||||||
for (int i = 0; i < copyLen; i++) {
|
for (int i = 0; i < copyLen; i++) {
|
||||||
pathPadded[i] = customPath[i];
|
pathPadded[i] = customPath[i];
|
||||||
}
|
}
|
||||||
@@ -598,9 +626,11 @@ int calculateLoRaAirtime({
|
|||||||
final crc = 1; // CRC enabled
|
final crc = 1; // CRC enabled
|
||||||
final de = lowDataRateOptimize ? 1 : 0;
|
final de = lowDataRateOptimize ? 1 : 0;
|
||||||
|
|
||||||
final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
|
final numerator =
|
||||||
|
8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
|
||||||
final denominator = 4 * (spreadingFactor - 2 * de);
|
final denominator = 4 * (spreadingFactor - 2 * de);
|
||||||
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4);
|
var payloadSymbols =
|
||||||
|
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
|
||||||
|
|
||||||
if (payloadSymbols < 0) {
|
if (payloadSymbols < 0) {
|
||||||
payloadSymbols = 8;
|
payloadSymbols = 8;
|
||||||
@@ -647,7 +677,8 @@ Uint8List buildSendCliCommandFrame(
|
|||||||
int attempt = 0,
|
int attempt = 0,
|
||||||
int? timestampSeconds,
|
int? timestampSeconds,
|
||||||
}) {
|
}) {
|
||||||
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
final timestamp =
|
||||||
|
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSendTxtMsg);
|
writer.writeByte(cmdSendTxtMsg);
|
||||||
writer.writeByte(txtTypeCliData);
|
writer.writeByte(txtTypeCliData);
|
||||||
@@ -661,10 +692,7 @@ Uint8List buildSendCliCommandFrame(
|
|||||||
|
|
||||||
// Build a telemetry request frame
|
// Build a telemetry request frame
|
||||||
// Format: [cmd][pub_key x32][payload]
|
// Format: [cmd][pub_key x32][payload]
|
||||||
Uint8List buildSendBinaryReq(
|
Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
||||||
Uint8List repeaterPubKey, {
|
|
||||||
Uint8List? payload,
|
|
||||||
}) {
|
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSendBinaryReq);
|
writer.writeByte(cmdSendBinaryReq);
|
||||||
writer.writeBytes(repeaterPubKey);
|
writer.writeBytes(repeaterPubKey);
|
||||||
@@ -672,4 +700,4 @@ Uint8List buildSendBinaryReq(
|
|||||||
writer.writeBytes(payload);
|
writer.writeBytes(payload);
|
||||||
}
|
}
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user