part of flutter_blue_plus;
class BluetoothDevice {
final DeviceIdentifier remoteId;
BluetoothDevice({
required this.remoteId,
});
BluetoothDevice.fromProto(BmBluetoothDevice p) : remoteId = p.remoteId;
BluetoothDevice.fromId(String remoteId) : remoteId = DeviceIdentifier(remoteId);
String get platformName => FlutterBluePlus._platformNames[remoteId] ?? "";
String get advName => FlutterBluePlus._advNames[remoteId] ?? "";
List<BluetoothService> get servicesList {
BmDiscoverServicesResult? result = FlutterBluePlus._knownServices[remoteId];
if (result == null) {
return [];
} else {
return result.services.map((p) => BluetoothService.fromProto(p)).toList();
}
}
void cancelWhenDisconnected(StreamSubscription subscription, {bool next = false, bool delayed = false}) {
if (isConnected == false && next == false) {
subscription.cancel();
} else if (delayed) {
FlutterBluePlus._delayedSubscriptions[remoteId] ??= [];
FlutterBluePlus._delayedSubscriptions[remoteId]!.add(subscription);
} else {
FlutterBluePlus._deviceSubscriptions[remoteId] ??= [];
FlutterBluePlus._deviceSubscriptions[remoteId]!.add(subscription);
}
}
bool get isAutoConnectEnabled {
return FlutterBluePlus._autoConnect.contains(remoteId);
}
bool get isConnected {
if (FlutterBluePlus._connectionStates[remoteId] == null) {
return false;
} else {
var state = FlutterBluePlus._connectionStates[remoteId]!.connectionState;
return state == BmConnectionStateEnum.connected;
}
}
bool get isDisconnected => isConnected == false;
Future<void> connect({
Duration timeout = const Duration(seconds: 35),
int? mtu = 512,
bool autoConnect = false,
}) async {
assert((mtu == null) || !autoConnect, "mtu and auto connect are incompatible");
_Mutex dmtx = _MutexFactory.getMutexForKey("disconnect");
bool dtook = await dmtx.take();
_Mutex mtx = _MutexFactory.getMutexForKey("global");
await mtx.take();
try {
if (autoConnect) {
FlutterBluePlus._autoConnect.add(remoteId);
}
var request = BmConnectRequest(
remoteId: remoteId,
autoConnect: autoConnect,
);
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnConnectionStateChanged")
.map((m) => m.arguments)
.map((args) => BmConnectionStateResponse.fromMap(args))
.where((p) => p.remoteId == remoteId);
Future<BmConnectionStateResponse> futureState = responseStream.first;
bool changed = await FlutterBluePlus._invokeMethod('connect', request.toMap());
dtook = dmtx.give();
if (changed && !autoConnect) {
BmConnectionStateResponse response = await futureState
.fbpEnsureAdapterIsOn("connect")
.fbpTimeout(timeout.inSeconds, "connect")
.catchError((e) async {
if (e is FlutterBluePlusException && e.code == FbpErrorCode.timeout.index) {
await FlutterBluePlus._invokeMethod('disconnect', remoteId.str);
}
throw e;
});
if (response.connectionState == BmConnectionStateEnum.disconnected) {
if (response.disconnectReasonCode == bmUserCanceledErrorCode) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "connect", FbpErrorCode.connectionCanceled.index, "connection canceled");
} else {
throw FlutterBluePlusException(
_nativeError, "connect", response.disconnectReasonCode, response.disconnectReasonString);
}
}
}
} finally {
if (dtook) {
dmtx.give();
}
mtx.give();
}
if (Platform.isAndroid && isConnected && mtu != null) {
await requestMtu(mtu);
}
}
Future<void> disconnect({int timeout = 35, bool queue = true}) async {
_Mutex dtx = _MutexFactory.getMutexForKey("disconnect");
await dtx.take();
_Mutex mtx = _MutexFactory.getMutexForKey("global");
if (queue) {
await mtx.take();
}
try {
FlutterBluePlus._autoConnect.remove(remoteId);
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnConnectionStateChanged")
.map((m) => m.arguments)
.map((args) => BmConnectionStateResponse.fromMap(args))
.where((p) => p.remoteId == remoteId)
.where((p) => p.connectionState == BmConnectionStateEnum.disconnected);
Future<BmConnectionStateResponse> futureState = responseStream.first;
bool changed = await FlutterBluePlus._invokeMethod('disconnect', remoteId.str);
if (changed) {
await futureState.fbpEnsureAdapterIsOn("disconnect").fbpTimeout(timeout, "disconnect");
}
} finally {
dtx.give();
if (queue) {
mtx.give();
}
}
}
Future<List<BluetoothService>> discoverServices({bool subscribeToServicesChanged = true, int timeout = 15}) async {
if (isDisconnected) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "discoverServices", FbpErrorCode.deviceIsDisconnected.index, "device is not connected");
}
_Mutex mtx = _MutexFactory.getMutexForKey("global");
await mtx.take();
List<BluetoothService> result = [];
try {
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnDiscoveredServices")
.map((m) => m.arguments)
.map((args) => BmDiscoverServicesResult.fromMap(args))
.where((p) => p.remoteId == remoteId);
Future<BmDiscoverServicesResult> futureResponse = responseStream.first;
await FlutterBluePlus._invokeMethod('discoverServices', remoteId.str);
BmDiscoverServicesResult response = await futureResponse
.fbpEnsureAdapterIsOn("discoverServices")
.fbpEnsureDeviceIsConnected(this, "discoverServices")
.fbpTimeout(timeout, "discoverServices");
if (!response.success) {
throw FlutterBluePlusException(_nativeError, "discoverServices", response.errorCode, response.errorString);
}
result = response.services.map((p) => BluetoothService.fromProto(p)).toList();
} finally {
mtx.give();
}
if (subscribeToServicesChanged) {
if (Platform.isIOS == false && Platform.isMacOS == false) {
BluetoothCharacteristic? c = _servicesChangedCharacteristic;
if (c != null && (c.properties.notify || c.properties.indicate) && c.isNotifying == false) {
await c.setNotifyValue(true);
}
}
}
return result;
}
DisconnectReason? get disconnectReason {
if (FlutterBluePlus._connectionStates[remoteId] == null) {
return null;
}
int? code = FlutterBluePlus._connectionStates[remoteId]!.disconnectReasonCode;
String? description = FlutterBluePlus._connectionStates[remoteId]!.disconnectReasonString;
return DisconnectReason(_nativeError, code, description);
}
Stream<BluetoothConnectionState> get connectionState {
BluetoothConnectionState initialValue = BluetoothConnectionState.disconnected;
if (FlutterBluePlus._connectionStates[remoteId] != null) {
initialValue = _bmToConnectionState(FlutterBluePlus._connectionStates[remoteId]!.connectionState);
}
return FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnConnectionStateChanged")
.map((m) => m.arguments)
.map((args) => BmConnectionStateResponse.fromMap(args))
.where((p) => p.remoteId == remoteId)
.map((p) => _bmToConnectionState(p.connectionState))
.newStreamWithInitialValue(initialValue);
}
int get mtuNow {
return FlutterBluePlus._mtuValues[remoteId]?.mtu ?? 23;
}
Stream<int> get mtu {
int initialValue = FlutterBluePlus._mtuValues[remoteId]?.mtu ?? 23;
return FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnMtuChanged")
.map((m) => m.arguments)
.map((args) => BmMtuChangedResponse.fromMap(args))
.where((p) => p.remoteId == remoteId)
.map((p) => p.mtu)
.newStreamWithInitialValue(initialValue);
}
Stream<void> get onServicesReset {
return FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnServicesReset")
.map((m) => m.arguments)
.map((args) => BmBluetoothDevice.fromMap(args))
.where((p) => p.remoteId == remoteId)
.map((m) => null);
}
Future<int> readRssi({int timeout = 15}) async {
if (isDisconnected) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "readRssi", FbpErrorCode.deviceIsDisconnected.index, "device is not connected");
}
_Mutex mtx = _MutexFactory.getMutexForKey("global");
await mtx.take();
int rssi = 0;
try {
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnReadRssi")
.map((m) => m.arguments)
.map((args) => BmReadRssiResult.fromMap(args))
.where((p) => (p.remoteId == remoteId));
Future<BmReadRssiResult> futureResponse = responseStream.first;
await FlutterBluePlus._invokeMethod('readRssi', remoteId.str);
BmReadRssiResult response = await futureResponse
.fbpEnsureAdapterIsOn("readRssi")
.fbpEnsureDeviceIsConnected(this, "readRssi")
.fbpTimeout(timeout, "readRssi");
if (!response.success) {
throw FlutterBluePlusException(_nativeError, "readRssi", response.errorCode, response.errorString);
}
rssi = response.rssi;
} finally {
mtx.give();
}
return rssi;
}
Future<int> requestMtu(int desiredMtu, {double predelay = 0.35, int timeout = 15}) async {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "requestMtu", FbpErrorCode.androidOnly.index, "android-only");
}
if (isDisconnected) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "requestMtu", FbpErrorCode.deviceIsDisconnected.index, "device is not connected");
}
_Mutex mtx = _MutexFactory.getMutexForKey("global");
await mtx.take();
if (predelay > 0) {
await Future.delayed(Duration(milliseconds: (predelay * 1000).toInt()));
}
var mtu = 0;
try {
var request = BmMtuChangeRequest(
remoteId: remoteId,
mtu: desiredMtu,
);
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnMtuChanged")
.map((m) => m.arguments)
.map((args) => BmMtuChangedResponse.fromMap(args))
.where((p) => p.remoteId == remoteId)
.map((p) => p.mtu);
Future<int> futureResponse = responseStream.first;
await FlutterBluePlus._invokeMethod('requestMtu', request.toMap());
mtu = await futureResponse
.fbpEnsureAdapterIsOn("requestMtu")
.fbpEnsureDeviceIsConnected(this, "requestMtu")
.fbpTimeout(timeout, "requestMtu");
} finally {
mtx.give();
}
return mtu;
}
Future<void> requestConnectionPriority({required ConnectionPriority connectionPriorityRequest}) async {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "requestConnectionPriority", FbpErrorCode.androidOnly.index, "android-only");
}
if (isDisconnected) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "requestConnectionPriority",
FbpErrorCode.deviceIsDisconnected.index, "device is not connected");
}
var request = BmConnectionPriorityRequest(
remoteId: remoteId,
connectionPriority: _bmFromConnectionPriority(connectionPriorityRequest),
);
await FlutterBluePlus._invokeMethod('requestConnectionPriority', request.toMap());
}
Future<void> setPreferredPhy({
required int txPhy,
required int rxPhy,
required PhyCoding option,
}) async {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "setPreferredPhy", FbpErrorCode.androidOnly.index, "android-only");
}
if (isDisconnected) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "setPreferredPhy", FbpErrorCode.deviceIsDisconnected.index, "device is not connected");
}
var request = BmPreferredPhy(
remoteId: remoteId,
txPhy: txPhy,
rxPhy: rxPhy,
phyOptions: option.index,
);
await FlutterBluePlus._invokeMethod('setPreferredPhy', request.toMap());
}
Future<void> createBond({int timeout = 90}) async {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "createBond", FbpErrorCode.androidOnly.index, "android-only");
}
if (isDisconnected) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "createBond", FbpErrorCode.deviceIsDisconnected.index, "device is not connected");
}
_Mutex mtx = _MutexFactory.getMutexForKey("global");
await mtx.take();
try {
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnBondStateChanged")
.map((m) => m.arguments)
.map((args) => BmBondStateResponse.fromMap(args))
.where((p) => p.remoteId == remoteId)
.where((p) => p.bondState != BmBondStateEnum.bonding);
Future<BmBondStateResponse> futureResponse = responseStream.first;
bool changed = await FlutterBluePlus._invokeMethod('createBond', remoteId.str);
if (changed) {
BmBondStateResponse bs = await futureResponse
.fbpEnsureAdapterIsOn("createBond")
.fbpEnsureDeviceIsConnected(this, "createBond")
.fbpTimeout(timeout, "createBond");
if (bs.bondState != BmBondStateEnum.bonded) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "createBond", FbpErrorCode.createBondFailed.hashCode,
"Failed to create bond. ${bs.bondState}");
}
}
} finally {
mtx.give();
}
}
Future<void> removeBond({int timeout = 30}) async {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "removeBond", FbpErrorCode.androidOnly.index, "android-only");
}
_Mutex mtx = _MutexFactory.getMutexForKey("global");
await mtx.take();
try {
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnBondStateChanged")
.map((m) => m.arguments)
.map((args) => BmBondStateResponse.fromMap(args))
.where((p) => p.remoteId == remoteId)
.where((p) => p.bondState != BmBondStateEnum.bonding);
Future<BmBondStateResponse> futureResponse = responseStream.first;
bool changed = await FlutterBluePlus._invokeMethod('removeBond', remoteId.str);
if (changed) {
BmBondStateResponse bs = await futureResponse
.fbpEnsureAdapterIsOn("removeBond")
.fbpEnsureDeviceIsConnected(this, "removeBond")
.fbpTimeout(timeout, "removeBond");
if (bs.bondState != BmBondStateEnum.none) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "createBond", FbpErrorCode.removeBondFailed.hashCode,
"Failed to remove bond. ${bs.bondState}");
}
}
} finally {
mtx.give();
}
}
Future<void> clearGattCache() async {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "clearGattCache", FbpErrorCode.androidOnly.index, "android-only");
}
if (isDisconnected) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "clearGattCache", FbpErrorCode.deviceIsDisconnected.index, "device is not connected");
}
await FlutterBluePlus._invokeMethod('clearGattCache', remoteId.str);
}
Stream<BluetoothBondState> get bondState async* {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "bondState", FbpErrorCode.androidOnly.index, "android-only");
}
if (FlutterBluePlus._bondStates[remoteId] == null) {
var val = await FlutterBluePlus._methodChannel
.invokeMethod('getBondState', remoteId.str)
.then((args) => BmBondStateResponse.fromMap(args));
if (FlutterBluePlus._bondStates[remoteId] == null) {
FlutterBluePlus._bondStates[remoteId] = val;
}
}
yield* FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnBondStateChanged")
.map((m) => m.arguments)
.map((args) => BmBondStateResponse.fromMap(args))
.where((p) => p.remoteId == remoteId)
.map((p) => _bmToBondState(p.bondState))
.newStreamWithInitialValue(_bmToBondState(FlutterBluePlus._bondStates[remoteId]!.bondState));
}
BluetoothBondState? get prevBondState {
var b = FlutterBluePlus._bondStates[remoteId]?.prevState;
return b != null ? _bmToBondState(b) : null;
}
BluetoothCharacteristic? get _servicesChangedCharacteristic {
final Guid gattUuid = Guid("1801");
final Guid servicesChangedUuid = Guid("2A05");
BluetoothService? gatt = servicesList._firstWhereOrNull((svc) => svc.uuid == gattUuid);
return gatt?.characteristics._firstWhereOrNull((chr) => chr.uuid == servicesChangedUuid);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is BluetoothDevice && runtimeType == other.runtimeType && remoteId == other.remoteId);
@override
int get hashCode => remoteId.hashCode;
@override
String toString() {
return 'BluetoothDevice{'
'remoteId: $remoteId, '
'platformName: $platformName, '
'services: ${FlutterBluePlus._knownServices[remoteId]}'
'}';
}
@Deprecated("removed. no replacement")
Stream<bool> get isDiscoveringServices async* {
yield false;
}
@Deprecated('Use createBond() instead')
Future<void> pair() async => await createBond();
@Deprecated('Use remoteId instead')
DeviceIdentifier get id => remoteId;
@Deprecated('Use platformName instead')
String get localName => platformName;
@Deprecated('Use platformName instead')
String get name => platformName;
@Deprecated('Use connectionState instead')
Stream<BluetoothConnectionState> get state => connectionState;
@Deprecated("removed. no replacement")
Stream<List<BluetoothService>> get servicesStream async* {
yield [];
}
@Deprecated("removed. no replacement")
Stream<List<BluetoothService>> get services async* {
yield [];
}
}