From 083b715039480497b483c2ab8cd7c36638a3dc02 Mon Sep 17 00:00:00 2001 From: Janez T Date: Fri, 13 Mar 2026 18:55:05 +0100 Subject: [PATCH] Add clearChannelMessages to Messages --- lib/providers/app_provider.dart | 3 +- lib/providers/connection_provider.dart | 89 +++++++++---------- ...connection_provider_channel_slot_test.dart | 8 ++ 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 7771600..cad7802 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -818,7 +818,8 @@ class AppProvider with ChangeNotifier { '🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"', ); - final isDeletedChannel = AppProvider.isDeletedChannelInfo( + final isDeletedChannel = connectionProvider + .shouldTreatChannelInfoAsDeleted( channelIdx, channelName, secret, diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart index 5133cb8..049ce66 100644 --- a/lib/providers/connection_provider.dart +++ b/lib/providers/connection_provider.dart @@ -92,6 +92,7 @@ class ConnectionProvider with ChangeNotifier { bool get isScanning => _isScanning; bool _isSpectrumScanActive = false; bool get isSpectrumScanActive => _isSpectrumScanActive; + final Set _pendingDeletedChannelIndices = {}; String? _error; String? get error => _error; @@ -903,6 +904,22 @@ class ConnectionProvider with ChangeNotifier { return message.contains('0x1f') && message.contains('timed out'); } + bool shouldTreatChannelInfoAsDeleted( + int channelIdx, + String channelName, + Uint8List secret, + ) { + final isEmptyInfo = + channelIdx != 0 && + channelName.isEmpty && + secret.every((byte) => byte == 0); + if (!isEmptyInfo) { + return false; + } + + return _pendingDeletedChannelIndices.remove(channelIdx); + } + Future findNextEmptyChannelSlot() async { if (!_activeService.isConnected) { throw Exception('Not connected to device'); @@ -915,58 +932,35 @@ class ConnectionProvider with ChangeNotifier { final maxChannels = _deviceInfo.maxChannels ?? 40; final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0; - // Match meshcore-open: trust the current synced channel list first and - // pick the first missing slot rather than probing every slot on-device. - if (getChannelInfo != null) { - final usedIndices = {}; - for (int i = 1; i < maxChannels; i++) { - final channel = getChannelInfo!(i); - if (channel == null) { - continue; - } - - if (_channelSlotIsOccupied(channel)) { - usedIndices.add(i); - } - } - - for (int i = 1; i < maxChannels; i++) { - if (!usedIndices.contains(i)) { - debugPrint(' ✅ Found empty slot from synced channels: $i'); - return i; - } - } - - debugPrint( - ' ⚠️ Synced channels report all custom slots occupied ($maxCustomChannels total)', - ); + // Match meshcore-open: choose the first missing slot from the synced + // channel set and avoid speculative per-slot probing that can overwrite + // an existing channel when device state is delayed or transient. + if (getChannelInfo == null) { + throw Exception('Channel state is unavailable'); } - // Check each slot starting from 1 (skip 0 = public channel) + final usedIndices = {}; for (int i = 1; i < maxChannels; i++) { - // First check cache - if (getChannelInfo != null) { - final channel = getChannelInfo!(i); - if (channel != null && _channelSlotIsOccupied(channel)) { - final channelName = (channel as dynamic).name as String?; - final label = (channelName != null && channelName.isNotEmpty) - ? channelName - : 'Channel $i'; - debugPrint(' ⏭️ Slot $i occupied: "$label"'); - continue; // Skip occupied slots - } + final channel = getChannelInfo!(i); + if (channel == null) { + continue; } - // Slot appears empty in cache, verify by querying device - debugPrint(' 🔍 Checking slot $i...'); - final isEmpty = await isChannelSlotEmpty(i); - if (isEmpty) { - debugPrint(' ✅ Found empty slot: $i'); + if (_channelSlotIsOccupied(channel)) { + usedIndices.add(i); + } + } + + for (int i = 1; i < maxChannels; i++) { + if (!usedIndices.contains(i)) { + debugPrint(' ✅ Found empty slot from synced channels: $i'); return i; } } - debugPrint(' ❌ All slots (1-${maxChannels - 1}) are in use'); + debugPrint( + ' ❌ Synced channels report all custom slots occupied ($maxCustomChannels total)', + ); return null; } catch (e) { debugPrint('❌ [Provider] Failed to find empty channel slot: $e'); @@ -998,12 +992,16 @@ class ConnectionProvider with ChangeNotifier { // Determine channel type final bool isHashChannel = channelName.startsWith('#'); + final maxChannels = _deviceInfo.maxChannels ?? 40; + + // Refresh channel state before choosing a slot so we match meshcore-open's + // "pick first missing slot from the synced list" behavior. + await _activeService.syncAllChannels(maxChannels: maxChannels); // Match meshcore-open behavior: // - deterministic hash channels (#name) cannot be duplicated // - private channels always use the next empty slot, even if the name matches if (getChannelInfo != null) { - final maxChannels = _deviceInfo.maxChannels ?? 40; for (int i = 1; i < maxChannels; i++) { final channel = getChannelInfo!(i); if (channel != null) { @@ -1028,7 +1026,6 @@ class ConnectionProvider with ChangeNotifier { } // Find next empty slot for any new channel. - final maxChannels = _deviceInfo.maxChannels ?? 40; final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0; final emptySlot = await findNextEmptyChannelSlot(); if (emptySlot == null) { @@ -1107,6 +1104,7 @@ class ConnectionProvider with ChangeNotifier { try { debugPrint('🗑️ [Provider] Deleting channel in slot $channelIdx...'); + _pendingDeletedChannelIndices.add(channelIdx); // Delete channel on device (sets empty name and zeroed secret) await _activeService.deleteChannel(channelIdx); @@ -1126,6 +1124,7 @@ class ConnectionProvider with ChangeNotifier { // The empty channel will trigger removal via onChannelInfoReceived callback await _activeService.getChannel(channelIdx); } catch (e) { + _pendingDeletedChannelIndices.remove(channelIdx); _error = 'Failed to delete channel: $e'; debugPrint('❌ [Provider] Channel deletion failed: $e'); notifyListeners(); diff --git a/test/providers/connection_provider_channel_slot_test.dart b/test/providers/connection_provider_channel_slot_test.dart index 9cae089..7996905 100644 --- a/test/providers/connection_provider_channel_slot_test.dart +++ b/test/providers/connection_provider_channel_slot_test.dart @@ -37,5 +37,13 @@ void main() { isFalse, ); }); + + test('does not treat arbitrary empty channel info as deletion', () { + final provider = ConnectionProvider(); + expect( + provider.shouldTreatChannelInfoAsDeleted(3, '', Uint8List(16)), + isFalse, + ); + }); }); }