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