diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 93a926c..0d2ff1f 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3900,6 +3900,18 @@ class MeshCoreConnector extends ChangeNotifier { } } + /// Whether a RESP_CODE_ERR should skip the in-flight channel slot. The + /// firmware answers ERR for an empty/invalid channel index; without this the + /// sync waits out its 2s timeout + 3 retries on every empty slot, so a device + /// with few populated channels out of a large capacity takes minutes to sync. + /// A pending generic-ack command (a SET / channel-text send) is order- + /// correlated to the ERR first, so only advance when none is waiting. (#82) + static bool shouldAdvanceChannelSyncOnError({ + required bool isSyncingChannels, + required bool channelSyncInFlight, + required bool hasPendingGenericAck, + }) => isSyncingChannels && channelSyncInFlight && !hasPendingGenericAck; + void _handleErrorFrame(Uint8List frame) { final errCode = frame.length > 1 ? frame[1] : -1; _appDebugLogService?.warn( @@ -3907,6 +3919,23 @@ class MeshCoreConnector extends ChangeNotifier { tag: 'Protocol', ); + // An in-flight channel GET that draws an ERR means that slot is empty — + // advance to the next index the instant the ERR lands instead of waiting + // out the timeout + retry budget (mirrors the CHANNEL_INFO success path + // minus adding a channel). This is the whole sync-slowness fix (#82). + if (shouldAdvanceChannelSyncOnError( + isSyncingChannels: _isSyncingChannels, + channelSyncInFlight: _channelSyncInFlight, + hasPendingGenericAck: _pendingGenericAckQueue.isNotEmpty, + )) { + _channelSyncTimeout?.cancel(); + _channelSyncInFlight = false; + _channelSyncRetries = 0; + _nextChannelIndexToRequest++; + unawaited(_requestNextChannel()); + return; + } + if (_pendingGenericAckQueue.isEmpty) { return; } diff --git a/test/connector/meshcore_connector_channel_sync_test.dart b/test/connector/meshcore_connector_channel_sync_test.dart new file mode 100644 index 0000000..4cb5fb3 --- /dev/null +++ b/test/connector/meshcore_connector_channel_sync_test.dart @@ -0,0 +1,62 @@ +// Channel-sync decision helper (#82). +// +// The firmware answers RESP_CODE_ERR for an empty/invalid channel index. The +// sync must skip that slot the instant the ERR arrives; without this branch it +// waits out the 2s timeout + 3 retries on EVERY empty slot, so a device with a +// few populated channels out of a large capacity takes minutes to sync (~8s per +// empty slot). This pins the decision: advance on ERR only while a channel GET +// is in flight AND no generic-ack command is waiting — a pending generic-ack +// (a SET / channel-text send) is order-correlated to the ERR first. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; + +void main() { + group('shouldAdvanceChannelSyncOnError', () { + test('advances when a channel GET is in flight and nothing else awaits the ' + 'ERR', () { + expect( + MeshCoreConnector.shouldAdvanceChannelSyncOnError( + isSyncingChannels: true, + channelSyncInFlight: true, + hasPendingGenericAck: false, + ), + isTrue, + ); + }); + + test('does not advance when not syncing channels', () { + expect( + MeshCoreConnector.shouldAdvanceChannelSyncOnError( + isSyncingChannels: false, + channelSyncInFlight: true, + hasPendingGenericAck: false, + ), + isFalse, + ); + }); + + test('does not advance when no channel GET is in flight', () { + expect( + MeshCoreConnector.shouldAdvanceChannelSyncOnError( + isSyncingChannels: true, + channelSyncInFlight: false, + hasPendingGenericAck: false, + ), + isFalse, + ); + }); + + test('a pending generic-ack command owns the ERR first (no channel ' + 'advance)', () { + expect( + MeshCoreConnector.shouldAdvanceChannelSyncOnError( + isSyncingChannels: true, + channelSyncInFlight: true, + hasPendingGenericAck: true, + ), + isFalse, + ); + }); + }); +}