feat: smarter firmware confirm via reconnect

This commit is contained in:
2026-05-04 14:40:13 +02:00
parent 16690dc216
commit bcccd03ecc
2 changed files with 172 additions and 17 deletions

View File

@ -140,6 +140,89 @@ void main() {
await transport.dispose();
});
test('completes when FINISH status is lost but bootloader disconnects',
() async {
final image = _validImage(80);
final transport = _FakeFirmwareUpdateTransport(
totalBytes: image.length,
suppressFinishStatus: true,
);
final service = FirmwareUpdateService(
transport: transport,
defaultStatusTimeout: const Duration(milliseconds: 20),
defaultPostFinishResetTimeout: const Duration(milliseconds: 100),
);
final result = await service.startUpdate(
imageBytes: image,
sessionId: 15,
);
expect(result.isOk(), isTrue);
expect(
transport.controlWrites.last, [universalShifterDfuOpcodeFinish, 15]);
expect(transport.steps, contains('reconnectForVerification'));
expect(transport.steps, contains('verifyDeviceReachable'));
expect(service.currentProgress.state, DfuUpdateState.completed);
await service.dispose();
await transport.dispose();
});
test('fails when FINISH status is lost and bootloader stays connected',
() async {
final image = _validImage(80);
final transport = _FakeFirmwareUpdateTransport(
totalBytes: image.length,
suppressFinishStatus: true,
disconnectAfterFinish: false,
);
final service = FirmwareUpdateService(
transport: transport,
defaultStatusTimeout: const Duration(milliseconds: 10),
defaultPostFinishResetTimeout: const Duration(milliseconds: 30),
);
final result = await service.startUpdate(
imageBytes: image,
sessionId: 16,
);
expect(result.isErr(), isTrue);
expect(result.unwrapErr().toString(), contains('post-FINISH reset'));
expect(service.currentProgress.state, DfuUpdateState.failed);
expect(transport.steps, isNot(contains('reconnectForVerification')));
await service.dispose();
await transport.dispose();
});
test('fails when FINISH returns explicit bootloader error', () async {
final image = _validImage(80);
final transport = _FakeFirmwareUpdateTransport(
totalBytes: image.length,
finishStatusCode: DfuBootloaderStatusCode.flashError,
);
final service = FirmwareUpdateService(
transport: transport,
defaultStatusTimeout: const Duration(milliseconds: 20),
defaultPostFinishResetTimeout: const Duration(milliseconds: 100),
);
final result = await service.startUpdate(
imageBytes: image,
sessionId: 17,
);
expect(result.isErr(), isTrue);
expect(result.unwrapErr().toString(), contains('flash error'));
expect(service.currentProgress.state, DfuUpdateState.failed);
expect(transport.steps, isNot(contains('reconnectForVerification')));
await service.dispose();
await transport.dispose();
});
test('reconnects and resumes from status after transient data failure',
() async {
final image = _validImage(130);
@ -297,6 +380,9 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
this.suppressFirstDataStatus = false,
this.failDataWriteAtOffsetOnce,
this.resetSessionOnRecoveryStatus = false,
this.suppressFinishStatus = false,
this.disconnectAfterFinish = true,
this.finishStatusCode = DfuBootloaderStatusCode.ok,
this.onDataWrite,
});
@ -308,6 +394,9 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
final bool suppressFirstDataStatus;
final int? failDataWriteAtOffsetOnce;
final bool resetSessionOnRecoveryStatus;
final bool suppressFinishStatus;
final bool disconnectAfterFinish;
final DfuBootloaderStatusCode finishStatusCode;
final void Function()? onDataWrite;
final StreamController<List<int>> _statusController =
@ -323,6 +412,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
bool _sentDataFailure = false;
bool _sentQueueFull = false;
bool _suppressedDataStatus = false;
bool _finishDisconnectAvailable = false;
@override
Future<Result<bool>> isConnectedToBootloader() async {
@ -389,7 +479,11 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
}
_scheduleStatus(DfuBootloaderStatusCode.ok, _sessionId, _expectedOffset);
} else if (opcode == universalShifterDfuOpcodeFinish) {
_scheduleStatus(DfuBootloaderStatusCode.ok, payload[1], totalBytes);
if (suppressFinishStatus) {
_finishDisconnectAvailable = disconnectAfterFinish;
} else {
_scheduleStatus(finishStatusCode, payload[1], totalBytes);
}
} else if (opcode == universalShifterDfuOpcodeAbort) {
_scheduleStatus(DfuBootloaderStatusCode.ok, payload[1], 0);
}
@ -431,7 +525,11 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
@override
Future<Result<void>> waitForBootloaderDisconnect(
{required Duration timeout}) async {
if (timeout == Duration.zero && !_finishDisconnectAvailable) {
return bail('still connected');
}
steps.add('waitForBootloaderDisconnect');
_finishDisconnectAvailable = true;
return Ok(null);
}