From 6047a02a6e8ad210cc0b935083d5598f2a055bb3 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 22 Jun 2026 21:12:05 -0400 Subject: [PATCH] fix(#82): skip empty channel slots on RESP_CODE_ERR instead of timing out The firmware answers RESP_CODE_ERR for an empty/invalid channel index, but _handleErrorFrame never advanced the in-flight channel sync. So every empty slot waited out the 2s timeout + 3 retries (~8s each): a device with a few populated channels out of a large capacity took minutes to sync (~40 slots requested, ~29 empty -> ~4 min) before the real channels (which answer CHANNEL_INFO) finally synced in the last couple seconds. Now an ERR that lands while a channel GET is in flight (and no generic-ack command is waiting -- that one is order-correlated to the ERR first) advances to the next slot immediately, mirroring the CHANNEL_INFO success path minus adding a channel. The decision is pinned as a pure static predicate and tested, matching the connector's existing decision-helper test convention. Co-Authored-By: Claude Opus 4.8 --- lib/connector/meshcore_connector.dart | 29 +++++++++ .../meshcore_connector_channel_sync_test.dart | 62 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 test/connector/meshcore_connector_channel_sync_test.dart 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, + ); + }); + }); +}