feat(dfu): add connection and MTU preflight checks
This commit is contained in:
@ -299,11 +299,20 @@ class BluetoothController {
|
|||||||
|
|
||||||
Future<Result<void>> requestMtu(String deviceId,
|
Future<Result<void>> requestMtu(String deviceId,
|
||||||
{int mtu = defaultMtu}) async {
|
{int mtu = defaultMtu}) async {
|
||||||
|
final result = await requestMtuAndGetValue(deviceId, mtu: mtu);
|
||||||
|
if (result.isErr()) {
|
||||||
|
return bail(result.unwrapErr());
|
||||||
|
}
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<int>> requestMtuAndGetValue(String deviceId,
|
||||||
|
{int mtu = defaultMtu}) async {
|
||||||
try {
|
try {
|
||||||
final negotiatedMtu = await _ble.requestMtu(deviceId: deviceId, mtu: mtu);
|
final negotiatedMtu = await _ble.requestMtu(deviceId: deviceId, mtu: mtu);
|
||||||
log.info(
|
log.info(
|
||||||
'MTU negotiated for $deviceId: requested $mtu, got $negotiatedMtu');
|
'MTU negotiated for $deviceId: requested $mtu, got $negotiatedMtu');
|
||||||
return Ok(null);
|
return Ok(negotiatedMtu);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return bail('Error requesting MTU $mtu for $deviceId: $e');
|
return bail('Error requesting MTU $mtu for $deviceId: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,10 @@ const int universalShifterDfuOpcodeAbort = 0x03;
|
|||||||
|
|
||||||
const int universalShifterDfuFrameSizeBytes = 64;
|
const int universalShifterDfuFrameSizeBytes = 64;
|
||||||
const int universalShifterDfuFramePayloadSizeBytes = 63;
|
const int universalShifterDfuFramePayloadSizeBytes = 63;
|
||||||
|
const int universalShifterAttWriteOverheadBytes = 3;
|
||||||
|
const int universalShifterDfuMinimumMtu =
|
||||||
|
universalShifterDfuFrameSizeBytes + universalShifterAttWriteOverheadBytes;
|
||||||
|
const int universalShifterDfuPreferredMtu = 128;
|
||||||
|
|
||||||
const int universalShifterDfuFlagEncrypted = 0x01;
|
const int universalShifterDfuFlagEncrypted = 0x01;
|
||||||
const int universalShifterDfuFlagSigned = 0x02;
|
const int universalShifterDfuFlagSigned = 0x02;
|
||||||
@ -107,6 +111,61 @@ class DfuUpdateProgress {
|
|||||||
state == DfuUpdateState.failed;
|
state == DfuUpdateState.failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DfuPreflightFailureReason {
|
||||||
|
deviceNotConnected,
|
||||||
|
wrongConnectedDevice,
|
||||||
|
mtuRequestFailed,
|
||||||
|
mtuTooLow,
|
||||||
|
}
|
||||||
|
|
||||||
|
class DfuPreflightResult {
|
||||||
|
const DfuPreflightResult._({
|
||||||
|
required this.requestedMtu,
|
||||||
|
required this.requiredMtu,
|
||||||
|
required this.negotiatedMtu,
|
||||||
|
required this.failureReason,
|
||||||
|
required this.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int requestedMtu;
|
||||||
|
final int requiredMtu;
|
||||||
|
final int? negotiatedMtu;
|
||||||
|
final DfuPreflightFailureReason? failureReason;
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
bool get canStart => failureReason == null;
|
||||||
|
|
||||||
|
static DfuPreflightResult ready({
|
||||||
|
required int requestedMtu,
|
||||||
|
required int negotiatedMtu,
|
||||||
|
int requiredMtu = universalShifterDfuMinimumMtu,
|
||||||
|
}) {
|
||||||
|
return DfuPreflightResult._(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
requiredMtu: requiredMtu,
|
||||||
|
negotiatedMtu: negotiatedMtu,
|
||||||
|
failureReason: null,
|
||||||
|
message: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DfuPreflightResult failed({
|
||||||
|
required int requestedMtu,
|
||||||
|
required DfuPreflightFailureReason failureReason,
|
||||||
|
required String message,
|
||||||
|
int requiredMtu = universalShifterDfuMinimumMtu,
|
||||||
|
int? negotiatedMtu,
|
||||||
|
}) {
|
||||||
|
return DfuPreflightResult._(
|
||||||
|
requestedMtu: requestedMtu,
|
||||||
|
requiredMtu: requiredMtu,
|
||||||
|
negotiatedMtu: negotiatedMtu,
|
||||||
|
failureReason: failureReason,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ShifterErrorInfo {
|
class ShifterErrorInfo {
|
||||||
const ShifterErrorInfo({
|
const ShifterErrorInfo({
|
||||||
required this.code,
|
required this.code,
|
||||||
|
|||||||
@ -6,12 +6,30 @@ import 'package:anyhow/anyhow.dart';
|
|||||||
|
|
||||||
class ShifterService {
|
class ShifterService {
|
||||||
ShifterService({
|
ShifterService({
|
||||||
required BluetoothController bluetooth,
|
BluetoothController? bluetooth,
|
||||||
required this.buttonDeviceId,
|
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 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 =
|
final StreamController<CentralStatus> _statusController =
|
||||||
StreamController<CentralStatus>.broadcast();
|
StreamController<CentralStatus>.broadcast();
|
||||||
@ -28,7 +46,7 @@ class ShifterService {
|
|||||||
Future<Result<void>> writeConnectToAddress(String bikeDeviceId) async {
|
Future<Result<void>> writeConnectToAddress(String bikeDeviceId) async {
|
||||||
try {
|
try {
|
||||||
final payload = parseMacToLittleEndianBytes(bikeDeviceId);
|
final payload = parseMacToLittleEndianBytes(bikeDeviceId);
|
||||||
return _bluetooth.writeCharacteristic(
|
return _requireBluetooth.writeCharacteristic(
|
||||||
buttonDeviceId,
|
buttonDeviceId,
|
||||||
universalShifterControlServiceUuid,
|
universalShifterControlServiceUuid,
|
||||||
universalShifterConnectToAddrCharacteristicUuid,
|
universalShifterConnectToAddrCharacteristicUuid,
|
||||||
@ -42,7 +60,7 @@ class ShifterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<void>> writeCommand(UniversalShifterCommand command) {
|
Future<Result<void>> writeCommand(UniversalShifterCommand command) {
|
||||||
return _bluetooth.writeCharacteristic(
|
return _requireBluetooth.writeCharacteristic(
|
||||||
buttonDeviceId,
|
buttonDeviceId,
|
||||||
universalShifterControlServiceUuid,
|
universalShifterControlServiceUuid,
|
||||||
universalShifterCommandCharacteristicUuid,
|
universalShifterCommandCharacteristicUuid,
|
||||||
@ -59,7 +77,7 @@ class ShifterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<GearRatiosData>> readGearRatios() async {
|
Future<Result<GearRatiosData>> readGearRatios() async {
|
||||||
final readRes = await _bluetooth.readCharacteristic(
|
final readRes = await _requireBluetooth.readCharacteristic(
|
||||||
buttonDeviceId,
|
buttonDeviceId,
|
||||||
universalShifterControlServiceUuid,
|
universalShifterControlServiceUuid,
|
||||||
universalShifterGearRatiosCharacteristicUuid,
|
universalShifterGearRatiosCharacteristicUuid,
|
||||||
@ -106,8 +124,10 @@ class ShifterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<void>> writeGearRatios(GearRatiosData data) async {
|
Future<Result<void>> writeGearRatios(GearRatiosData data) async {
|
||||||
final mtuResult =
|
final mtuResult = await _requireBluetooth.requestMtu(
|
||||||
await _bluetooth.requestMtu(buttonDeviceId, mtu: _gearRatioWriteMtu);
|
buttonDeviceId,
|
||||||
|
mtu: _gearRatioWriteMtu,
|
||||||
|
);
|
||||||
if (mtuResult.isErr()) {
|
if (mtuResult.isErr()) {
|
||||||
return bail(
|
return bail(
|
||||||
'Could not request MTU before writing gear ratios: ${mtuResult.unwrapErr()}');
|
'Could not request MTU before writing gear ratios: ${mtuResult.unwrapErr()}');
|
||||||
@ -124,7 +144,7 @@ class ShifterService {
|
|||||||
payload[_defaultGearIndexOffset] =
|
payload[_defaultGearIndexOffset] =
|
||||||
limit == 0 ? 0 : data.defaultGearIndex.clamp(0, limit - 1).toInt();
|
limit == 0 ? 0 : data.defaultGearIndex.clamp(0, limit - 1).toInt();
|
||||||
|
|
||||||
return _bluetooth.writeCharacteristic(
|
return _requireBluetooth.writeCharacteristic(
|
||||||
buttonDeviceId,
|
buttonDeviceId,
|
||||||
universalShifterControlServiceUuid,
|
universalShifterControlServiceUuid,
|
||||||
universalShifterGearRatiosCharacteristicUuid,
|
universalShifterGearRatiosCharacteristicUuid,
|
||||||
@ -133,7 +153,7 @@ class ShifterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<CentralStatus>> readStatus() async {
|
Future<Result<CentralStatus>> readStatus() async {
|
||||||
final readRes = await _bluetooth.readCharacteristic(
|
final readRes = await _requireBluetooth.readCharacteristic(
|
||||||
buttonDeviceId,
|
buttonDeviceId,
|
||||||
universalShifterControlServiceUuid,
|
universalShifterControlServiceUuid,
|
||||||
universalShifterStatusCharacteristicUuid,
|
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() {
|
void startStatusNotifications() {
|
||||||
if (_statusSubscription != null) {
|
if (_statusSubscription != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_statusSubscription = _bluetooth
|
_statusSubscription = _requireBluetooth
|
||||||
.subscribeToCharacteristic(
|
.subscribeToCharacteristic(
|
||||||
buttonDeviceId,
|
buttonDeviceId,
|
||||||
universalShifterControlServiceUuid,
|
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 {
|
class GearRatiosData {
|
||||||
const GearRatiosData({
|
const GearRatiosData({
|
||||||
required this.ratios,
|
required this.ratios,
|
||||||
|
|||||||
137
test/service/dfu_preflight_test.dart
Normal file
137
test/service/dfu_preflight_test.dart
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:abawo_bt_app/service/shifter_service.dart';
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ShifterService.runDfuPreflight', () {
|
||||||
|
test('fails when no active button connection exists', () async {
|
||||||
|
final adapter = _FakeDfuPreflightBluetoothAdapter(
|
||||||
|
currentConnectionState: (ConnectionStatus.disconnected, null),
|
||||||
|
mtuResult: Ok(128),
|
||||||
|
);
|
||||||
|
final service = ShifterService(
|
||||||
|
buttonDeviceId: 'target-device',
|
||||||
|
dfuPreflightBluetooth: adapter,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await service.runDfuPreflight();
|
||||||
|
|
||||||
|
expect(result.isOk(), isTrue);
|
||||||
|
final preflight = result.unwrap();
|
||||||
|
expect(preflight.canStart, isFalse);
|
||||||
|
expect(preflight.failureReason,
|
||||||
|
DfuPreflightFailureReason.deviceNotConnected);
|
||||||
|
expect(adapter.requestMtuCallCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fails when connected to a different button', () async {
|
||||||
|
final adapter = _FakeDfuPreflightBluetoothAdapter(
|
||||||
|
currentConnectionState: (ConnectionStatus.connected, 'wrong-device'),
|
||||||
|
mtuResult: Ok(128),
|
||||||
|
);
|
||||||
|
final service = ShifterService(
|
||||||
|
buttonDeviceId: 'target-device',
|
||||||
|
dfuPreflightBluetooth: adapter,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await service.runDfuPreflight();
|
||||||
|
|
||||||
|
expect(result.isOk(), isTrue);
|
||||||
|
final preflight = result.unwrap();
|
||||||
|
expect(preflight.canStart, isFalse);
|
||||||
|
expect(preflight.failureReason,
|
||||||
|
DfuPreflightFailureReason.wrongConnectedDevice);
|
||||||
|
expect(adapter.requestMtuCallCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fails when MTU negotiation fails', () async {
|
||||||
|
final adapter = _FakeDfuPreflightBluetoothAdapter(
|
||||||
|
currentConnectionState: (ConnectionStatus.connected, 'target-device'),
|
||||||
|
mtuResult: bail('adapter rejected mtu request'),
|
||||||
|
);
|
||||||
|
final service = ShifterService(
|
||||||
|
buttonDeviceId: 'target-device',
|
||||||
|
dfuPreflightBluetooth: adapter,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await service.runDfuPreflight(requestedMtu: 247);
|
||||||
|
|
||||||
|
expect(result.isOk(), isTrue);
|
||||||
|
final preflight = result.unwrap();
|
||||||
|
expect(preflight.canStart, isFalse);
|
||||||
|
expect(
|
||||||
|
preflight.failureReason, DfuPreflightFailureReason.mtuRequestFailed);
|
||||||
|
expect(preflight.message, contains('adapter rejected mtu request'));
|
||||||
|
expect(adapter.requestedMtuValues, [247]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fails when negotiated MTU is too low for 64-byte frame writes',
|
||||||
|
() async {
|
||||||
|
final adapter = _FakeDfuPreflightBluetoothAdapter(
|
||||||
|
currentConnectionState: (ConnectionStatus.connected, 'target-device'),
|
||||||
|
mtuResult: Ok(universalShifterDfuMinimumMtu - 1),
|
||||||
|
);
|
||||||
|
final service = ShifterService(
|
||||||
|
buttonDeviceId: 'target-device',
|
||||||
|
dfuPreflightBluetooth: adapter,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await service.runDfuPreflight();
|
||||||
|
|
||||||
|
expect(result.isOk(), isTrue);
|
||||||
|
final preflight = result.unwrap();
|
||||||
|
expect(preflight.canStart, isFalse);
|
||||||
|
expect(preflight.failureReason, DfuPreflightFailureReason.mtuTooLow);
|
||||||
|
expect(preflight.negotiatedMtu, universalShifterDfuMinimumMtu - 1);
|
||||||
|
expect(preflight.requiredMtu, universalShifterDfuMinimumMtu);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes when connected to target and MTU is sufficient', () async {
|
||||||
|
final adapter = _FakeDfuPreflightBluetoothAdapter(
|
||||||
|
currentConnectionState: (ConnectionStatus.connected, 'target-device'),
|
||||||
|
mtuResult: Ok(128),
|
||||||
|
);
|
||||||
|
final service = ShifterService(
|
||||||
|
buttonDeviceId: 'target-device',
|
||||||
|
dfuPreflightBluetooth: adapter,
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await service.runDfuPreflight();
|
||||||
|
|
||||||
|
expect(result.isOk(), isTrue);
|
||||||
|
final preflight = result.unwrap();
|
||||||
|
expect(preflight.canStart, isTrue);
|
||||||
|
expect(preflight.failureReason, isNull);
|
||||||
|
expect(preflight.negotiatedMtu, 128);
|
||||||
|
expect(preflight.requestedMtu, universalShifterDfuPreferredMtu);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeDfuPreflightBluetoothAdapter
|
||||||
|
implements DfuPreflightBluetoothAdapter {
|
||||||
|
_FakeDfuPreflightBluetoothAdapter({
|
||||||
|
required this.currentConnectionState,
|
||||||
|
required Result<int> mtuResult,
|
||||||
|
}) : _mtuResult = mtuResult;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final (ConnectionStatus, String?) currentConnectionState;
|
||||||
|
|
||||||
|
final Result<int> _mtuResult;
|
||||||
|
|
||||||
|
int requestMtuCallCount = 0;
|
||||||
|
final List<int> requestedMtuValues = <int>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<int>> requestMtuAndGetValue(
|
||||||
|
String deviceId, {
|
||||||
|
required int mtu,
|
||||||
|
}) async {
|
||||||
|
requestMtuCallCount += 1;
|
||||||
|
requestedMtuValues.add(mtu);
|
||||||
|
return _mtuResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user