Add clearChannelMessages to Messages

This commit is contained in:
Janez T
2026-03-13 18:55:05 +01:00
parent 36584be0ab
commit 083b715039
3 changed files with 54 additions and 46 deletions

View File

@@ -818,7 +818,8 @@ class AppProvider with ChangeNotifier {
'🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"', '🔔 [AppProvider] onChannelInfoReceived called: idx=$channelIdx, name="$channelName"',
); );
final isDeletedChannel = AppProvider.isDeletedChannelInfo( final isDeletedChannel = connectionProvider
.shouldTreatChannelInfoAsDeleted(
channelIdx, channelIdx,
channelName, channelName,
secret, secret,

View File

@@ -92,6 +92,7 @@ class ConnectionProvider with ChangeNotifier {
bool get isScanning => _isScanning; bool get isScanning => _isScanning;
bool _isSpectrumScanActive = false; bool _isSpectrumScanActive = false;
bool get isSpectrumScanActive => _isSpectrumScanActive; bool get isSpectrumScanActive => _isSpectrumScanActive;
final Set<int> _pendingDeletedChannelIndices = <int>{};
String? _error; String? _error;
String? get error => _error; String? get error => _error;
@@ -903,6 +904,22 @@ class ConnectionProvider with ChangeNotifier {
return message.contains('0x1f') && message.contains('timed out'); 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<int?> findNextEmptyChannelSlot() async { Future<int?> findNextEmptyChannelSlot() async {
if (!_activeService.isConnected) { if (!_activeService.isConnected) {
throw Exception('Not connected to device'); throw Exception('Not connected to device');
@@ -915,58 +932,35 @@ class ConnectionProvider with ChangeNotifier {
final maxChannels = _deviceInfo.maxChannels ?? 40; final maxChannels = _deviceInfo.maxChannels ?? 40;
final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0; final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0;
// Match meshcore-open: trust the current synced channel list first and // Match meshcore-open: choose the first missing slot from the synced
// pick the first missing slot rather than probing every slot on-device. // channel set and avoid speculative per-slot probing that can overwrite
if (getChannelInfo != null) { // an existing channel when device state is delayed or transient.
final usedIndices = <int>{}; if (getChannelInfo == null) {
for (int i = 1; i < maxChannels; i++) { throw Exception('Channel state is unavailable');
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)',
);
} }
// Check each slot starting from 1 (skip 0 = public channel) final usedIndices = <int>{};
for (int i = 1; i < maxChannels; i++) { for (int i = 1; i < maxChannels; i++) {
// First check cache final channel = getChannelInfo!(i);
if (getChannelInfo != null) { if (channel == null) {
final channel = getChannelInfo!(i); continue;
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
}
} }
// Slot appears empty in cache, verify by querying device if (_channelSlotIsOccupied(channel)) {
debugPrint(' 🔍 Checking slot $i...'); usedIndices.add(i);
final isEmpty = await isChannelSlotEmpty(i); }
if (isEmpty) { }
debugPrint(' ✅ Found empty slot: $i');
for (int i = 1; i < maxChannels; i++) {
if (!usedIndices.contains(i)) {
debugPrint(' ✅ Found empty slot from synced channels: $i');
return i; return i;
} }
} }
debugPrint(' ❌ All slots (1-${maxChannels - 1}) are in use'); debugPrint(
' ❌ Synced channels report all custom slots occupied ($maxCustomChannels total)',
);
return null; return null;
} catch (e) { } catch (e) {
debugPrint('❌ [Provider] Failed to find empty channel slot: $e'); debugPrint('❌ [Provider] Failed to find empty channel slot: $e');
@@ -998,12 +992,16 @@ class ConnectionProvider with ChangeNotifier {
// Determine channel type // Determine channel type
final bool isHashChannel = channelName.startsWith('#'); 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: // Match meshcore-open behavior:
// - deterministic hash channels (#name) cannot be duplicated // - deterministic hash channels (#name) cannot be duplicated
// - private channels always use the next empty slot, even if the name matches // - private channels always use the next empty slot, even if the name matches
if (getChannelInfo != null) { if (getChannelInfo != null) {
final maxChannels = _deviceInfo.maxChannels ?? 40;
for (int i = 1; i < maxChannels; i++) { for (int i = 1; i < maxChannels; i++) {
final channel = getChannelInfo!(i); final channel = getChannelInfo!(i);
if (channel != null) { if (channel != null) {
@@ -1028,7 +1026,6 @@ class ConnectionProvider with ChangeNotifier {
} }
// Find next empty slot for any new channel. // Find next empty slot for any new channel.
final maxChannels = _deviceInfo.maxChannels ?? 40;
final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0; final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0;
final emptySlot = await findNextEmptyChannelSlot(); final emptySlot = await findNextEmptyChannelSlot();
if (emptySlot == null) { if (emptySlot == null) {
@@ -1107,6 +1104,7 @@ class ConnectionProvider with ChangeNotifier {
try { try {
debugPrint('🗑️ [Provider] Deleting channel in slot $channelIdx...'); debugPrint('🗑️ [Provider] Deleting channel in slot $channelIdx...');
_pendingDeletedChannelIndices.add(channelIdx);
// Delete channel on device (sets empty name and zeroed secret) // Delete channel on device (sets empty name and zeroed secret)
await _activeService.deleteChannel(channelIdx); await _activeService.deleteChannel(channelIdx);
@@ -1126,6 +1124,7 @@ class ConnectionProvider with ChangeNotifier {
// The empty channel will trigger removal via onChannelInfoReceived callback // The empty channel will trigger removal via onChannelInfoReceived callback
await _activeService.getChannel(channelIdx); await _activeService.getChannel(channelIdx);
} catch (e) { } catch (e) {
_pendingDeletedChannelIndices.remove(channelIdx);
_error = 'Failed to delete channel: $e'; _error = 'Failed to delete channel: $e';
debugPrint('❌ [Provider] Channel deletion failed: $e'); debugPrint('❌ [Provider] Channel deletion failed: $e');
notifyListeners(); notifyListeners();

View File

@@ -37,5 +37,13 @@ void main() {
isFalse, isFalse,
); );
}); });
test('does not treat arbitrary empty channel info as deletion', () {
final provider = ConnectionProvider();
expect(
provider.shouldTreatChannelInfoAsDeleted(3, '', Uint8List(16)),
isFalse,
);
});
}); });
} }