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 <noreply@anthropic.com>
pull/99/head
Strycher 4 weeks ago
parent 31c0674330
commit 6047a02a6e

@ -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;
}

@ -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,
);
});
});
}
Loading…
Cancel
Save

Powered by TurnKey Linux.