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 (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) {});
}
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 (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 [];
}
}