feat(dfu): add connection and MTU preflight checks
This commit is contained in:
@ -6,12 +6,30 @@ import 'package:anyhow/anyhow.dart';
|
||||
|
||||
class ShifterService {
|
||||
ShifterService({
|
||||
required BluetoothController bluetooth,
|
||||
BluetoothController? bluetooth,
|
||||
required this.buttonDeviceId,
|
||||
}) : _bluetooth = bluetooth;
|
||||
DfuPreflightBluetoothAdapter? dfuPreflightBluetooth,
|
||||
}) : _bluetooth = bluetooth,
|
||||
_dfuPreflightBluetooth =
|
||||
dfuPreflightBluetooth ?? _BluetoothDfuPreflightAdapter(bluetooth!) {
|
||||
if (bluetooth == null && dfuPreflightBluetooth == null) {
|
||||
throw ArgumentError(
|
||||
'Either bluetooth or dfuPreflightBluetooth must be provided.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final BluetoothController _bluetooth;
|
||||
final BluetoothController? _bluetooth;
|
||||
final String buttonDeviceId;
|
||||
final DfuPreflightBluetoothAdapter _dfuPreflightBluetooth;
|
||||
|
||||
BluetoothController get _requireBluetooth {
|
||||
final bluetooth = _bluetooth;
|
||||
if (bluetooth == null) {
|
||||
throw StateError('Bluetooth controller is not available.');
|
||||
}
|
||||
return bluetooth;
|
||||
}
|
||||
|
||||
final StreamController<CentralStatus> _statusController =
|
||||
StreamController<CentralStatus>.broadcast();
|
||||
@ -28,7 +46,7 @@ class ShifterService {
|
||||
Future<Result<void>> writeConnectToAddress(String bikeDeviceId) async {
|
||||
try {
|
||||
final payload = parseMacToLittleEndianBytes(bikeDeviceId);
|
||||
return _bluetooth.writeCharacteristic(
|
||||
return _requireBluetooth.writeCharacteristic(
|
||||
buttonDeviceId,
|
||||
universalShifterControlServiceUuid,
|
||||
universalShifterConnectToAddrCharacteristicUuid,
|
||||
@ -42,7 +60,7 @@ class ShifterService {
|
||||
}
|
||||
|
||||
Future<Result<void>> writeCommand(UniversalShifterCommand command) {
|
||||
return _bluetooth.writeCharacteristic(
|
||||
return _requireBluetooth.writeCharacteristic(
|
||||
buttonDeviceId,
|
||||
universalShifterControlServiceUuid,
|
||||
universalShifterCommandCharacteristicUuid,
|
||||
@ -59,7 +77,7 @@ class ShifterService {
|
||||
}
|
||||
|
||||
Future<Result<GearRatiosData>> readGearRatios() async {
|
||||
final readRes = await _bluetooth.readCharacteristic(
|
||||
final readRes = await _requireBluetooth.readCharacteristic(
|
||||
buttonDeviceId,
|
||||
universalShifterControlServiceUuid,
|
||||
universalShifterGearRatiosCharacteristicUuid,
|
||||
@ -106,8 +124,10 @@ class ShifterService {
|
||||
}
|
||||
|
||||
Future<Result<void>> writeGearRatios(GearRatiosData data) async {
|
||||
final mtuResult =
|
||||
await _bluetooth.requestMtu(buttonDeviceId, mtu: _gearRatioWriteMtu);
|
||||
final mtuResult = await _requireBluetooth.requestMtu(
|
||||
buttonDeviceId,
|
||||
mtu: _gearRatioWriteMtu,
|
||||
);
|
||||
if (mtuResult.isErr()) {
|
||||
return bail(
|
||||
'Could not request MTU before writing gear ratios: ${mtuResult.unwrapErr()}');
|
||||
@ -124,7 +144,7 @@ class ShifterService {
|
||||
payload[_defaultGearIndexOffset] =
|
||||
limit == 0 ? 0 : data.defaultGearIndex.clamp(0, limit - 1).toInt();
|
||||
|
||||
return _bluetooth.writeCharacteristic(
|
||||
return _requireBluetooth.writeCharacteristic(
|
||||
buttonDeviceId,
|
||||
universalShifterControlServiceUuid,
|
||||
universalShifterGearRatiosCharacteristicUuid,
|
||||
@ -133,7 +153,7 @@ class ShifterService {
|
||||
}
|
||||
|
||||
Future<Result<CentralStatus>> readStatus() async {
|
||||
final readRes = await _bluetooth.readCharacteristic(
|
||||
final readRes = await _requireBluetooth.readCharacteristic(
|
||||
buttonDeviceId,
|
||||
universalShifterControlServiceUuid,
|
||||
universalShifterStatusCharacteristicUuid,
|
||||
@ -149,12 +169,78 @@ class ShifterService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result<DfuPreflightResult>> runDfuPreflight({
|
||||
int requestedMtu = universalShifterDfuPreferredMtu,
|
||||
}) async {
|
||||
final currentConnection = _dfuPreflightBluetooth.currentConnectionState;
|
||||
final connectionStatus = currentConnection.$1;
|
||||
final connectedDeviceId = currentConnection.$2;
|
||||
|
||||
if (connectionStatus != ConnectionStatus.connected ||
|
||||
connectedDeviceId == null) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
failureReason: DfuPreflightFailureReason.deviceNotConnected,
|
||||
message:
|
||||
'No button connection is active. Connect the target button, then retry the firmware update.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (connectedDeviceId != buttonDeviceId) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
failureReason: DfuPreflightFailureReason.wrongConnectedDevice,
|
||||
message:
|
||||
'Connected to a different button ($connectedDeviceId). Reconnect to $buttonDeviceId before starting the firmware update.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final mtuResult = await _dfuPreflightBluetooth.requestMtuAndGetValue(
|
||||
buttonDeviceId,
|
||||
mtu: requestedMtu,
|
||||
);
|
||||
if (mtuResult.isErr()) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
failureReason: DfuPreflightFailureReason.mtuRequestFailed,
|
||||
message:
|
||||
'Could not negotiate BLE MTU for DFU. Move closer to the button and retry. Details: ${mtuResult.unwrapErr()}',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final negotiatedMtu = mtuResult.unwrap();
|
||||
if (negotiatedMtu < universalShifterDfuMinimumMtu) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
negotiatedMtu: negotiatedMtu,
|
||||
failureReason: DfuPreflightFailureReason.mtuTooLow,
|
||||
message:
|
||||
'Negotiated MTU $negotiatedMtu is too small for 64-byte DFU frames. Need at least $universalShifterDfuMinimumMtu bytes MTU (64-byte frame + $universalShifterAttWriteOverheadBytes-byte ATT overhead). Reconnect and retry.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(
|
||||
DfuPreflightResult.ready(
|
||||
requestedMtu: requestedMtu,
|
||||
negotiatedMtu: negotiatedMtu,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void startStatusNotifications() {
|
||||
if (_statusSubscription != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_statusSubscription = _bluetooth
|
||||
_statusSubscription = _requireBluetooth
|
||||
.subscribeToCharacteristic(
|
||||
buttonDeviceId,
|
||||
universalShifterControlServiceUuid,
|
||||
@ -202,6 +288,32 @@ class ShifterService {
|
||||
}
|
||||
}
|
||||
|
||||
abstract interface class DfuPreflightBluetoothAdapter {
|
||||
(ConnectionStatus, String?) get currentConnectionState;
|
||||
Future<Result<int>> requestMtuAndGetValue(
|
||||
String deviceId, {
|
||||
required int mtu,
|
||||
});
|
||||
}
|
||||
|
||||
class _BluetoothDfuPreflightAdapter implements DfuPreflightBluetoothAdapter {
|
||||
const _BluetoothDfuPreflightAdapter(this._bluetooth);
|
||||
|
||||
final BluetoothController _bluetooth;
|
||||
|
||||
@override
|
||||
(ConnectionStatus, String?) get currentConnectionState =>
|
||||
_bluetooth.currentConnectionState;
|
||||
|
||||
@override
|
||||
Future<Result<int>> requestMtuAndGetValue(
|
||||
String deviceId, {
|
||||
required int mtu,
|
||||
}) {
|
||||
return _bluetooth.requestMtuAndGetValue(deviceId, mtu: mtu);
|
||||
}
|
||||
}
|
||||
|
||||
class GearRatiosData {
|
||||
const GearRatiosData({
|
||||
required this.ratios,
|
||||
|
||||
Reference in New Issue
Block a user