part of flutter_blue_plus;
class FlutterBluePlus {
static bool _initialized = false;
static final MethodChannel _methodChannel = const MethodChannel('flutter_blue_plus/methods');
static final StreamController<MethodCall> _methodStream = StreamController.broadcast();
static final Map<DeviceIdentifier, BmConnectionStateResponse> _connectionStates = {};
static final Map<DeviceIdentifier, BmDiscoverServicesResult> _knownServices = {};
static final Map<DeviceIdentifier, BmBondStateResponse> _bondStates = {};
static final Map<DeviceIdentifier, BmMtuChangedResponse> _mtuValues = {};
static final Map<DeviceIdentifier, String> _platformNames = {};
static final Map<DeviceIdentifier, String> _advNames = {};
static final Map<DeviceIdentifier, Map<String, List<int>>> _lastChrs = {};
static final Map<DeviceIdentifier, Map<String, List<int>>> _lastDescs = {};
static final Map<DeviceIdentifier, List<StreamSubscription>> _deviceSubscriptions = {};
static final Map<DeviceIdentifier, List<StreamSubscription>> _delayedSubscriptions = {};
static final List<StreamSubscription> _scanSubscriptions = [];
static final Set<DeviceIdentifier> _autoConnect = {};
static final _isScanning = _StreamControllerReEmit<bool>(initialValue: false);
static final _scanResults = _StreamControllerReEmit<List<ScanResult>>(initialValue: []);
static _BufferStream<BmScanResponse>? _scanBuffer;
static StreamSubscription<BmScanResponse?>? _scanSubscription;
static Timer? _scanTimeout;
static BmAdapterStateEnum? _adapterStateNow;
static LogLevel _logLevel = LogLevel.debug;
static bool _logColor = true;
static LogLevel get logLevel => _logLevel;
static Future<bool> get isSupported async => await _invokeMethod('isSupported');
static BluetoothAdapterState get adapterStateNow =>
_adapterStateNow != null ? _bmToAdapterState(_adapterStateNow!) : BluetoothAdapterState.unknown;
static Future<String> get adapterName async => await _invokeMethod('getAdapterName');
static Stream<bool> get isScanning => _isScanning.stream;
static bool get isScanningNow => _isScanning.latestValue;
static List<ScanResult> get lastScanResults => _scanResults.latestValue;
static Stream<List<ScanResult>> get scanResults => _scanResults.stream;
static Stream<List<ScanResult>> get onScanResults {
if (isScanningNow) {
return _scanResults.stream;
} else {
return _scanResults.stream.skip(1).newStreamWithInitialValue([]);
}
}
static final BluetoothEvents events = BluetoothEvents();
static Future<void> setOptions({
bool showPowerAlert = true,
}) async {
await _invokeMethod('setOptions', {"show_power_alert": showPowerAlert});
}
static Future<void> turnOn({int timeout = 60}) async {
var responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnTurnOnResponse")
.map((m) => m.arguments)
.map((args) => BmTurnOnResponse.fromMap(args));
Future<BmTurnOnResponse> futureResponse = responseStream.first;
bool changed = await _invokeMethod('turnOn');
if (changed) {
BmTurnOnResponse response = await futureResponse.fbpTimeout(timeout, "turnOn");
if (response.userAccepted == false) {
throw FlutterBluePlusException(ErrorPlatform.fbp, "turnOn", FbpErrorCode.userRejected.index, "user rejected");
}
await adapterState.where((s) => s == BluetoothAdapterState.on).first.fbpTimeout(timeout, "turnOn");
}
}
static Stream<BluetoothAdapterState> get adapterState async* {
if (_adapterStateNow == null) {
var result = await _invokeMethod('getAdapterState');
var value = BmBluetoothAdapterState.fromMap(result).adapterState;
if (_adapterStateNow == null) {
_adapterStateNow = value;
}
}
yield* FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnAdapterStateChanged")
.map((m) => m.arguments)
.map((args) => BmBluetoothAdapterState.fromMap(args))
.map((s) => _bmToAdapterState(s.adapterState))
.newStreamWithInitialValue(_bmToAdapterState(_adapterStateNow!));
}
static List<BluetoothDevice> get connectedDevices {
var copy = Map.from(_connectionStates);
copy.removeWhere((key, value) => value.connectionState == BmConnectionStateEnum.disconnected);
return copy.values.map((v) => BluetoothDevice(remoteId: v.remoteId)).toList();
}
static Future<List<BluetoothDevice>> get systemDevices async {
var result = await _invokeMethod('getSystemDevices');
var r = BmDevicesList.fromMap(result);
for (BmBluetoothDevice device in r.devices) {
if (device.platformName != null) {
_platformNames[device.remoteId] = device.platformName!;
}
}
return r.devices.map((d) => BluetoothDevice.fromProto(d)).toList();
}
static Future<List<BluetoothDevice>> get bondedDevices async {
var result = await _invokeMethod('getBondedDevices');
var r = BmDevicesList.fromMap(result);
for (BmBluetoothDevice device in r.devices) {
if (device.platformName != null) {
_platformNames[device.remoteId] = device.platformName!;
}
}
return r.devices.map((d) => BluetoothDevice.fromProto(d)).toList();
}
static Future<void> startScan({
List<Guid> withServices = const [],
List<String> withRemoteIds = const [],
List<String> withNames = const [],
List<String> withKeywords = const [],
List<MsdFilter> withMsd = const [],
List<ServiceDataFilter> withServiceData = const [],
Duration? timeout,
Duration? removeIfGone,
bool continuousUpdates = false,
int continuousDivisor = 1,
bool oneByOne = false,
AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
bool androidUsesFineLocation = false,
}) async {
assert(removeIfGone == null || continuousUpdates, "removeIfGone requires continuousUpdates");
assert(removeIfGone == null || !oneByOne, "removeIfGone is not compatible with oneByOne");
assert(continuousDivisor >= 1, "divisor must be >= 1");
bool hasOtherFilter = withServices.isNotEmpty ||
withRemoteIds.isNotEmpty ||
withNames.isNotEmpty ||
withMsd.isNotEmpty ||
withServiceData.isNotEmpty;
assert(!(Platform.isAndroid && withKeywords.isNotEmpty && hasOtherFilter),
"withKeywords is not compatible with other filters on Android");
_Mutex mtx = _MutexFactory.getMutexForKey("scan");
await mtx.take();
try {
if (_isScanning.latestValue == true) {
await _stopScan();
}
_isScanning.add(true);
var settings = BmScanSettings(
withServices: withServices,
withRemoteIds: withRemoteIds,
withNames: withNames,
withKeywords: withKeywords,
withMsd: withMsd.map((d) => d._bm).toList(),
withServiceData: withServiceData.map((d) => d._bm).toList(),
continuousUpdates: continuousUpdates,
continuousDivisor: continuousDivisor,
androidScanMode: androidScanMode.value,
androidUsesFineLocation: androidUsesFineLocation);
Stream<BmScanResponse> responseStream = FlutterBluePlus._methodStream.stream
.where((m) => m.method == "OnScanResponse")
.map((m) => m.arguments)
.map((args) => BmScanResponse.fromMap(args));
_scanBuffer = _BufferStream.listen(responseStream);
await _invokeMethod('startScan', settings.toMap()).onError((e, s) => _stopScan(invokePlatform: false));
late Stream<BmScanResponse?> outputStream = removeIfGone != null
? _mergeStreams([_scanBuffer!.stream, Stream.periodic(Duration(milliseconds: 250))])
: _scanBuffer!.stream;
_scanResults.add([]);
List<ScanResult> output = [];
_scanSubscription = outputStream.listen((BmScanResponse? response) {
if (response == null) {
if (output._removeWhere((elm) => DateTime.now().difference(elm.timeStamp) > removeIfGone!)) {
_scanResults.add(List.from(output));
}
} else {
if (response.success == false) {
var e = FlutterBluePlusException(_nativeError, "scan", response.errorCode, response.errorString);
_scanResults.addError(e);
_stopScan(invokePlatform: false);
}
for (BmScanAdvertisement bm in response.advertisements) {
if (bm.platformName != null) {
_platformNames[bm.remoteId] = bm.platformName!;
}
if (bm.advName != null) {
_advNames[bm.remoteId] = bm.advName!;
}
ScanResult sr = ScanResult.fromProto(bm);
if (oneByOne) {
_scanResults.add([sr]);
} else {
output.addOrUpdate(sr);
}
}
if (!oneByOne) {
_scanResults.add(List.from(output));
}
}
});
if (timeout != null) {
_scanTimeout = Timer(timeout, stopScan);
}
} finally {
mtx.give();
}
}
static Future<void> stopScan() async {
_Mutex mtx = _MutexFactory.getMutexForKey("scan");
await mtx.take();
try {
if(isScanningNow) {
await _stopScan();
} else if (_logLevel.index >= LogLevel.info.index) {
print("[FBP] stopScan: already stopped");
}
} finally {
mtx.give();
}
}
static Future<void> _stopScan({bool invokePlatform = true}) async {
_scanBuffer?.close();
_scanSubscription?.cancel();
_scanTimeout?.cancel();
_isScanning.add(false);
for (var subscription in _scanSubscriptions) {
subscription.cancel();
}
if (invokePlatform) {
await _invokeMethod('stopScan');
}
}
static void cancelWhenScanComplete(StreamSubscription subscription) {
FlutterBluePlus._scanSubscriptions.add(subscription);
}
static Future<void> setLogLevel(LogLevel level, {color = true}) async {
_logLevel = level;
_logColor = color;
await _invokeMethod('setLogLevel', level.index);
}
static Future<PhySupport> getPhySupport() async {
if (Platform.isAndroid == false) {
throw FlutterBluePlusException(
ErrorPlatform.fbp, "getPhySupport", FbpErrorCode.androidOnly.index, "android-only");
}
return await _invokeMethod('getPhySupport').then((args) => PhySupport.fromMap(args));
}
static Future<dynamic> _initFlutterBluePlus() async {
if (_initialized) {
return;
}
_initialized = true;
_methodChannel.setMethodCallHandler(_methodCallHandler);
if ((await _methodChannel.invokeMethod('flutterRestart')) != 0) {
await Future.delayed(Duration(milliseconds: 50));
while ((await _methodChannel.invokeMethod('connectedCount')) != 0) {
await Future.delayed(Duration(milliseconds: 50));
}
}
}
static Future<dynamic> _methodCallHandler(MethodCall call) async {
if (logLevel == LogLevel.verbose) {
String func = '[[ ${call.method} ]]';
String result = call.arguments.toString();
func = _logColor ? _black(func) : func;
result = _logColor ? _brown(result) : result;
print("[FBP] $func result: $result");
}
if (call.method == "OnDetachedFromEngine") {
_stopScan(invokePlatform: false);
}
if (call.method == "OnAdapterStateChanged") {
BmBluetoothAdapterState r = BmBluetoothAdapterState.fromMap(call.arguments);
_adapterStateNow = r.adapterState;
if (isScanningNow && r.adapterState != BmAdapterStateEnum.on) {
_stopScan(invokePlatform: false);
}
if (r.adapterState == BmAdapterStateEnum.on) {
for (DeviceIdentifier d in _autoConnect) {
BluetoothDevice(remoteId: d).connect(autoConnect: true, mtu: null).onError((e, s) {
if (logLevel != LogLevel.none) {
print("[FBP] [AutoConnect] connection failed: $e");
}
});
}
}
}
if (call.method == "OnConnectionStateChanged") {
var r = BmConnectionStateResponse.fromMap(call.arguments);
_connectionStates[r.remoteId] = r;
if (r.connectionState == BmConnectionStateEnum.disconnected) {
if (_mtuValues.containsKey(r.remoteId)) {
var resp = BmMtuChangedResponse(remoteId: r.remoteId, mtu: 23);
_methodStream.add(MethodCall("OnMtuChanged", resp.toMap()));
}
_mtuValues.remove(r.remoteId);
_lastDescs.remove(r.remoteId);
_lastChrs.remove(r.remoteId);
_deviceSubscriptions[r.remoteId]?.forEach((s) => s.cancel());
_deviceSubscriptions.remove(r.remoteId);
if (Platform.isAndroid == false) {
if (_autoConnect.contains(r.remoteId)) {
if (_adapterStateNow == BmAdapterStateEnum.on) {
var d = BluetoothDevice(remoteId: r.remoteId);
d.connect(autoConnect: true, mtu: null).onError((e, s) {
if (logLevel != LogLevel.none) {
print("[FBP] [AutoConnect] connection failed: $e");
}
});
}
}
}
}
}
if (call.method == "OnNameChanged") {
var device = BmNameChanged.fromMap(call.arguments);
if (Platform.isMacOS || Platform.isIOS) {
_platformNames[device.remoteId] = device.name;
}
}
if (call.method == "OnServicesReset") {
var r = BmBluetoothDevice.fromMap(call.arguments);
_knownServices.remove(r.remoteId);
}
if (call.method == "OnBondStateChanged") {
var r = BmBondStateResponse.fromMap(call.arguments);
_bondStates[r.remoteId] = r;
}
if (call.method == "OnDiscoveredServices") {
var r = BmDiscoverServicesResult.fromMap(call.arguments);
if (r.success == true) {
_knownServices[r.remoteId] = r;
}
}
if (call.method == "OnMtuChanged") {
var r = BmMtuChangedResponse.fromMap(call.arguments);
if (r.success == true) {
_mtuValues[r.remoteId] = r;
}
}
if (call.method == "OnCharacteristicReceived" || call.method == "OnCharacteristicWritten") {
var r = BmCharacteristicData.fromMap(call.arguments);
if (r.success == true) {
_lastChrs[r.remoteId] ??= {};
_lastChrs[r.remoteId]!["${r.serviceUuid}:${r.characteristicUuid}"] = r.value;
}
}
if (call.method == "OnDescriptorRead" || call.method == "OnDescriptorWritten") {
var r = BmDescriptorData.fromMap(call.arguments);
if (r.success == true) {
_lastDescs[r.remoteId] ??= {};
_lastDescs[r.remoteId]!["${r.serviceUuid}:${r.characteristicUuid}:${r.descriptorUuid}"] = r.value;
}
}
_methodStream.add(call);
if (call.method == "OnConnectionStateChanged") {
if (_delayedSubscriptions.isNotEmpty) {
var r = BmConnectionStateResponse.fromMap(call.arguments);
if (r.connectionState == BmConnectionStateEnum.disconnected) {
var remoteId = r.remoteId;
Future.delayed(Duration.zero).then((_) {
_delayedSubscriptions[remoteId]?.forEach((s) => s.cancel());
_delayedSubscriptions.remove(remoteId);
});
}
}
}
}
static Future<dynamic> _invokeMethod(
String method, [
dynamic arguments,
]) async {
dynamic out;
_Mutex mtx = _MutexFactory.getMutexForKey("invokeMethod");
await mtx.take();
try {
if (method != "setOptions") {
_initFlutterBluePlus();
}
if (logLevel == LogLevel.verbose) {
String func = '<$method>';
String args = arguments.toString();
func = _logColor ? _black(func) : func;
args = _logColor ? _magenta(args) : args;
print("[FBP] $func args: $args");
}
out = await _methodChannel.invokeMethod(method, arguments);
if (logLevel == LogLevel.verbose) {
String func = '<$method>';
String result = out.toString();
func = _logColor ? _black(func) : func;
result = _logColor ? _brown(result) : result;
print("[FBP] $func result: $result");
}
} finally {
mtx.give();
}
return out;
}
@Deprecated('Deprecated in Android SDK 33 with no replacement')
static Future<void> turnOff({int timeout = 10}) async {
Stream<BluetoothAdapterState> responseStream = adapterState.where((s) => s == BluetoothAdapterState.off);
Future<BluetoothAdapterState> futureResponse = responseStream.first;
await _invokeMethod('turnOff');
await futureResponse.fbpTimeout(timeout, "turnOff");
}
@Deprecated('Use adapterState.first == BluetoothAdapterState.on instead')
static Future<bool> get isOn async => await adapterState.first == BluetoothAdapterState.on;
@Deprecated('Use adapterName instead')
static Future<String> get name => adapterName;
@Deprecated('Use adapterState instead')
static Stream<BluetoothAdapterState> get state => adapterState;
@Deprecated('Use systemDevices instead')
static Future<List<BluetoothDevice>> get connectedSystemDevices => systemDevices;
@Deprecated('No longer needed, remove this from your code')
static void get instance => null;
@Deprecated('Use isSupported instead')
static Future<bool> get isAvailable async => await isSupported;
@Deprecated('removed. read MIGRATION.md for simple alternatives')
static Stream<ScanResult> scan() => throw Exception;
}
enum LogLevel {
none,
error,
warning,
info,
debug,
verbose,
}
class AndroidScanMode {
const AndroidScanMode(this.value);
static const lowPower = AndroidScanMode(0);
static const balanced = AndroidScanMode(1);
static const lowLatency = AndroidScanMode(2);
static const opportunistic = AndroidScanMode(-1);
final int value;
}
class MsdFilter {
int manufacturerId;
List<int> data;
List<int> mask;
MsdFilter(this.manufacturerId, {this.data = const [], this.mask = const []});
BmMsdFilter get _bm {
assert(mask.isEmpty || (data.length == mask.length), "mask & data must be same length");
return BmMsdFilter(manufacturerId, data, mask);
}
}
class ServiceDataFilter {
Guid service;
List<int> data;
List<int> mask;
ServiceDataFilter(this.service, {this.data = const [], this.mask = const []});
BmServiceDataFilter get _bm {
assert(mask.isEmpty || (data.length == mask.length), "mask & data must be same length");
return BmServiceDataFilter(service, data, mask);
}
}
class DeviceIdentifier {
final String str;
const DeviceIdentifier(this.str);
@override
String toString() => str;
@override
int get hashCode => str.hashCode;
@override
bool operator ==(other) => other is DeviceIdentifier && _compareAsciiLowerCase(str, other.str) == 0;
@Deprecated('Use str instead')
String get id => str;
}
class ScanResult {
final BluetoothDevice device;
final AdvertisementData advertisementData;
final int rssi;
final DateTime timeStamp;
ScanResult({
required this.device,
required this.advertisementData,
required this.rssi,
required this.timeStamp,
});
ScanResult.fromProto(BmScanAdvertisement p)
: device = BluetoothDevice(remoteId: p.remoteId),
advertisementData = AdvertisementData.fromProto(p),
rssi = p.rssi,
timeStamp = DateTime.now();
@override
bool operator ==(Object other) =>
identical(this, other) || other is ScanResult && runtimeType == other.runtimeType && device == other.device;
@override
int get hashCode => device.hashCode;
@override
String toString() {
return 'ScanResult{'
'device: $device, '
'advertisementData: $advertisementData, '
'rssi: $rssi, '
'timeStamp: $timeStamp'
'}';
}
}
class AdvertisementData {
final String advName;
final int? txPowerLevel;
final int? appearance;
final bool connectable;
final Map<int, List<int>> manufacturerData;
final Map<Guid, List<int>> serviceData;
final List<Guid> serviceUuids;
List<List<int>> get msd {
List<List<int>> out = [];
manufacturerData.forEach((key, value) {
out.add([key & 0xFF, (key >> 8) & 0xFF] + value);
});
return out;
}
AdvertisementData({
required this.advName,
required this.txPowerLevel,
required this.appearance,
required this.connectable,
required this.manufacturerData,
required this.serviceData,
required this.serviceUuids,
});
AdvertisementData.fromProto(BmScanAdvertisement p)
: advName = p.advName ?? "",
txPowerLevel = p.txPowerLevel,
appearance = p.appearance,
connectable = p.connectable,
manufacturerData = p.manufacturerData,
serviceData = p.serviceData,
serviceUuids = p.serviceUuids;
@override
String toString() {
return 'AdvertisementData{'
'advName: $advName, '
'txPowerLevel: $txPowerLevel, '
'appearance: $appearance, '
'connectable: $connectable, '
'manufacturerData: $manufacturerData, '
'serviceData: $serviceData, '
'serviceUuids: $serviceUuids'
'}';
}
@Deprecated('use advName instead')
String get localName => advName;
}
class PhySupport {
final bool le2M;
final bool leCoded;
PhySupport({required this.le2M, required this.leCoded});
factory PhySupport.fromMap(Map<dynamic, dynamic> json) {
return PhySupport(
le2M: json['le_2M'],
leCoded: json['le_coded'],
);
}
}
enum ErrorPlatform {
fbp,
android,
apple,
}
final ErrorPlatform _nativeError = (() {
if (Platform.isAndroid) {
return ErrorPlatform.android;
} else {
return ErrorPlatform.apple;
}
})();
enum FbpErrorCode {
success,
timeout,
androidOnly,
applePlatformOnly,
createBondFailed,
removeBondFailed,
deviceIsDisconnected,
serviceNotFound,
characteristicNotFound,
adapterIsOff,
connectionCanceled,
userRejected
}
class FlutterBluePlusException implements Exception {
final ErrorPlatform platform;
final String function;
final int? code;
final String? description;
FlutterBluePlusException(this.platform, this.function, this.code, this.description);
@override
String toString() {
String sPlatform = platform.toString().split('.').last;
return 'FlutterBluePlusException | $function | $sPlatform-code: $code | $description';
}
@Deprecated('Use function instead')
String get errorName => function;
@Deprecated('Use code instead')
int? get errorCode => code;
@Deprecated('Use description instead')
String? get errorString => description;
}