feat(dfu): verify reconnect before reporting update success
This commit is contained in:
@ -14,12 +14,18 @@ class FirmwareUpdateService {
|
||||
this.defaultWindowSize = 8,
|
||||
this.maxNoProgressRetries = 5,
|
||||
this.defaultAckTimeout = const Duration(milliseconds: 800),
|
||||
this.defaultPostFinishResetTimeout = const Duration(seconds: 8),
|
||||
this.defaultReconnectTimeout = const Duration(seconds: 12),
|
||||
this.defaultVerificationTimeout = const Duration(seconds: 5),
|
||||
}) : _transport = transport;
|
||||
|
||||
final FirmwareUpdateTransport _transport;
|
||||
final int defaultWindowSize;
|
||||
final int maxNoProgressRetries;
|
||||
final Duration defaultAckTimeout;
|
||||
final Duration defaultPostFinishResetTimeout;
|
||||
final Duration defaultReconnectTimeout;
|
||||
final Duration defaultVerificationTimeout;
|
||||
|
||||
final StreamController<DfuUpdateProgress> _progressController =
|
||||
StreamController<DfuUpdateProgress>.broadcast();
|
||||
@ -59,6 +65,9 @@ class FirmwareUpdateService {
|
||||
int? windowSize,
|
||||
Duration? ackTimeout,
|
||||
int? noProgressRetries,
|
||||
Duration? postFinishResetTimeout,
|
||||
Duration? reconnectTimeout,
|
||||
Duration? verificationTimeout,
|
||||
}) async {
|
||||
if (_isRunning) {
|
||||
return bail(
|
||||
@ -73,6 +82,12 @@ class FirmwareUpdateService {
|
||||
final effectiveAckTimeout = ackTimeout ?? defaultAckTimeout;
|
||||
final effectiveNoProgressRetries =
|
||||
noProgressRetries ?? maxNoProgressRetries;
|
||||
final effectivePostFinishResetTimeout =
|
||||
postFinishResetTimeout ?? defaultPostFinishResetTimeout;
|
||||
final effectiveReconnectTimeout =
|
||||
reconnectTimeout ?? defaultReconnectTimeout;
|
||||
final effectiveVerificationTimeout =
|
||||
verificationTimeout ?? defaultVerificationTimeout;
|
||||
|
||||
if (effectiveWindowSize <= 0) {
|
||||
return bail(
|
||||
@ -231,6 +246,38 @@ class FirmwareUpdateService {
|
||||
);
|
||||
}
|
||||
|
||||
await _ackSubscription?.cancel();
|
||||
_ackSubscription = null;
|
||||
|
||||
final resetDisconnectResult =
|
||||
await _transport.waitForExpectedResetDisconnect(
|
||||
timeout: effectivePostFinishResetTimeout,
|
||||
);
|
||||
if (resetDisconnectResult.isErr()) {
|
||||
throw _DfuFailure(
|
||||
'Device did not perform the expected post-FINISH reset disconnect: ${resetDisconnectResult.unwrapErr()}',
|
||||
);
|
||||
}
|
||||
|
||||
final reconnectResult = await _transport.reconnectForVerification(
|
||||
timeout: effectiveReconnectTimeout,
|
||||
);
|
||||
if (reconnectResult.isErr()) {
|
||||
throw _DfuFailure(
|
||||
'Device did not reconnect after DFU reset: ${reconnectResult.unwrapErr()}',
|
||||
);
|
||||
}
|
||||
|
||||
final verificationResult = await _transport.verifyDeviceReachable(
|
||||
timeout: effectiveVerificationTimeout,
|
||||
);
|
||||
if (verificationResult.isErr()) {
|
||||
throw _DfuFailure(
|
||||
'Device reconnected but post-update verification failed: ${verificationResult.unwrapErr()} '
|
||||
'Firmware version cannot be compared yet because the device does not expose a version characteristic.',
|
||||
);
|
||||
}
|
||||
|
||||
shouldAbortForCleanup = false;
|
||||
_emitProgress(
|
||||
state: DfuUpdateState.completed, sentBytes: imageBytes.length);
|
||||
@ -486,6 +533,22 @@ abstract interface class FirmwareUpdateTransport {
|
||||
Future<Result<void>> writeControl(List<int> payload);
|
||||
|
||||
Future<Result<void>> writeDataFrame(List<int> frame);
|
||||
|
||||
Future<Result<void>> waitForExpectedResetDisconnect({
|
||||
required Duration timeout,
|
||||
});
|
||||
|
||||
Future<Result<void>> reconnectForVerification({
|
||||
required Duration timeout,
|
||||
});
|
||||
|
||||
/// Verifies that the device is reachable after reconnect.
|
||||
///
|
||||
/// Current limitation: strict firmware version comparison is not possible
|
||||
/// yet because no firmware version characteristic is exposed by the device.
|
||||
Future<Result<void>> verifyDeviceReachable({
|
||||
required Duration timeout,
|
||||
});
|
||||
}
|
||||
|
||||
class ShifterFirmwareUpdateTransport implements FirmwareUpdateTransport {
|
||||
@ -535,6 +598,82 @@ class ShifterFirmwareUpdateTransport implements FirmwareUpdateTransport {
|
||||
withResponse: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> waitForExpectedResetDisconnect({
|
||||
required Duration timeout,
|
||||
}) async {
|
||||
final currentState = bluetoothController.currentConnectionState;
|
||||
if (currentState.$1 == ConnectionStatus.disconnected) {
|
||||
return Ok(null);
|
||||
}
|
||||
|
||||
try {
|
||||
await bluetoothController.connectionStateStream
|
||||
.firstWhere((state) => state.$1 == ConnectionStatus.disconnected)
|
||||
.timeout(timeout);
|
||||
return Ok(null);
|
||||
} on TimeoutException {
|
||||
return bail(
|
||||
'Timed out after ${timeout.inMilliseconds}ms waiting for the expected reset disconnect.',
|
||||
);
|
||||
} catch (error) {
|
||||
return bail('Failed while waiting for expected reset disconnect: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> reconnectForVerification({
|
||||
required Duration timeout,
|
||||
}) async {
|
||||
final connectResult =
|
||||
await bluetoothController.connectById(buttonDeviceId, timeout: timeout);
|
||||
if (connectResult.isErr()) {
|
||||
return bail(connectResult.unwrapErr());
|
||||
}
|
||||
|
||||
final currentState = bluetoothController.currentConnectionState;
|
||||
if (currentState.$1 == ConnectionStatus.connected &&
|
||||
currentState.$2 == buttonDeviceId) {
|
||||
return Ok(null);
|
||||
}
|
||||
|
||||
try {
|
||||
await bluetoothController.connectionStateStream
|
||||
.firstWhere(
|
||||
(state) =>
|
||||
state.$1 == ConnectionStatus.connected &&
|
||||
state.$2 == buttonDeviceId,
|
||||
)
|
||||
.timeout(timeout);
|
||||
return Ok(null);
|
||||
} on TimeoutException {
|
||||
return bail(
|
||||
'Timed out after ${timeout.inMilliseconds}ms waiting for reconnect.',
|
||||
);
|
||||
} catch (error) {
|
||||
return bail('Reconnect wait failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> verifyDeviceReachable({
|
||||
required Duration timeout,
|
||||
}) async {
|
||||
try {
|
||||
final statusResult = await shifterService.readStatus().timeout(timeout);
|
||||
if (statusResult.isErr()) {
|
||||
return bail(statusResult.unwrapErr());
|
||||
}
|
||||
return Ok(null);
|
||||
} on TimeoutException {
|
||||
return bail(
|
||||
'Timed out after ${timeout.inMilliseconds}ms while reading status for post-update verification.',
|
||||
);
|
||||
} catch (error) {
|
||||
return bail('Post-update verification failed: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _DfuFailure implements Exception {
|
||||
|
||||
Reference in New Issue
Block a user