我正在开发 Flutter 应用程序,并且正在用 Swift 编写一些平台代码,以便能够连接到经典蓝牙和 BLE 设备。
不幸的是,Flutter 包是针对 BLE 的。
截至 2019 年,Apple 的 BluetoothCore 可与 Classic/BDR/EDR 设备配合使用,如库文档中所示:https://developer.apple.com/videos/play/wwdc2019/901
从 IOS 13 开始支持,我正在使用 IOS 17.4.1 进行测试。
然而,即使尝试复制文档中的步骤,我也根本无法:
我知道诸如“设备必须处于广告模式”之类的事情。
请记住,我无法与设备提供商、制造商等联系以获得任何类型的 ID。
从代码中删除打印以节省空间。
让我们从我的 Swift 实现开始,从主 AppDelegate 类开始:
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
// Channels that send requests from Flutter to IOS, and receive responses or execute actions
let bluetoothChannel: FlutterMethodChannel = FlutterMethodChannel(name: "bluetooth_channel", binaryMessenger: controller.binaryMessenger)
// Channels that communicate automatically from IOS to Flutter (EventChannels)
let cbManagerStateChannel = FlutterEventChannel(name: "cbmanager_state_channel", binaryMessenger: controller.binaryMessenger)
let didDiscoverChannel = FlutterEventChannel(name: "did_discover_channel", binaryMessenger: controller.binaryMessenger)
// other channels...
// Handler classes for EventChannels
let cbManagerStateController: CBManagerStateController = CBManagerStateController()
let didDiscoverController: DidDiscoverController = DidDiscoverController()
// other handler classes...
// Manager classes for Channels
let bluetoothManager: BluetoothManager = BluetoothManager(cbManagerStateController: cbManagerStateController, didDiscoverController: didDiscoverController, didConnectController: didConnectController, didFailToConnectController: didFailToConnectController, didDisconnectController: didDisconnectController, connectionEventDidOccurController: connectionEventDidOccurController)
// Connecting channels and controllers
cbManagerStateChannel.setStreamHandler(cbManagerStateController)
didDiscoverChannel.setStreamHandler(didDiscoverController)
// other connections...
// Registering all available methods for bluetoothChannel
bluetoothChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "getCBManagerState":
result(bluetoothManager.getCBManagerState())
case "getCBManagerAuthorization":
result(bluetoothManager.getCBManagerAuthorization())
// other cases...
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
接下来,这是我的 BluetoothManager 类,它实现了所有委托方法:
class BluetoothManager: NSObject, CBCentralManagerDelegate {
private var centralManager: CBCentralManager
private var cbManagerAuthorization: CBManagerAuthorization = CBManagerAuthorization.notDetermined
private var scannedPeripheralList: [CBPeripheral]
private let cbManagerStateController: CBManagerStateController
private var didDiscoverController: DidDiscoverController
private var didConnectController: DidConnectController
private var didFailToConnectController: DidFailToConnectController
private var didDisconnectController: DidDisconnectController
private var connectionEventDidOccurController: ConnectionEventDidOccurController
init(cbManagerStateController: CBManagerStateController, didDiscoverController: DidDiscoverController, didConnectController: DidConnectController, didFailToConnectController: DidFailToConnectController, didDisconnectController: DidDisconnectController, connectionEventDidOccurController: ConnectionEventDidOccurController) {
self.centralManager = CBCentralManager(delegate: nil, queue: nil)
self.scannedPeripheralList = []
self.cbManagerStateController = cbManagerStateController
self.didDiscoverController = didDiscoverController
self.didConnectController = didConnectController
self.didFailToConnectController = didFailToConnectController
self.didDisconnectController = didDisconnectController
self.connectionEventDidOccurController = connectionEventDidOccurController
super.init()
centralManager.delegate = self
centralManager.registerForConnectionEvents(options: [:])
}
// Methods directly called by the Flutter side //
func getCBManagerState() -> Int {
return centralManager.state.rawValue
}
func getCBManagerAuthorization() -> Int {
return cbManagerAuthorization.rawValue
}
func getIsScanning() -> Bool {
return centralManager.isScanning
}
func getConnectedPeripherals() -> [Dictionary<String, String>] {
var devices:[[String : String]] = [Dictionary<String, String>]()
let connectedPeripherals: [CBPeripheral] = centralManager.retrieveConnectedPeripherals(withServices: [])
connectedPeripherals.forEach { peripheral in
let name: String = peripheral.name ?? "Unknown Name"
let peripheralId: String = peripheral.identifier.uuidString
devices.append([
"name": name,
"id": peripheralId
])
}
return devices
}
// Returns nothing, instead it calls centralManager(didDiscover)
func scanPeripherals() {
let serviceUUIDs: [CBUUID]? = nil
centralManager.scanForPeripherals(withServices: serviceUUIDs, options: nil)
}
// Returns nothing, To confirm if scan state actually stopped, call getIsScanning()
func stopScan() {
centralManager.stopScan()
scannedPeripheralList.removeAll()
}
// Returns nothing, instead it calls centralManager(didConnect) or (didFailToConnect)
func connectToPeripheral(_ peripheralIdString: String) {
guard let peripheralIdString: UUID = UUID(uuidString: peripheralIdString) else {
return
}
let peripherals:[CBPeripheral] = scannedPeripheralList
if let peripheral: CBPeripheral = peripherals.first(where: { $0.identifier == peripheralIdString }) {
centralManager.connect(peripheral)
}
}
func disconnectFromPeripheral(_ deviceIdString: String) {
guard let deviceId: UUID = UUID(uuidString: deviceIdString) else {
return
}
let peripherals: [CBPeripheral] = centralManager.retrieveConnectedPeripherals(withServices: [])
if let peripheral: CBPeripheral = peripherals.first(where: {$0.identifier == deviceId}) {
centralManager.cancelPeripheralConnection(peripheral)
}
}
// Methods called by the Swift side, we only keep open streams and channels on Flutter
// Called automatically when scanPeripherals() discovers a peripheral device during a scan
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
var name: String?
if let peripheralName: String = peripheral.name {
name = peripheralName
} else if let advertisementName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
name = advertisementName
}
let peripheralId: String = peripheral.identifier.uuidString
let peripheralState: Int = peripheral.state.rawValue
if (name != nil && !scannedPeripheralList.contains(where: { $0.identifier == peripheral.identifier })){
let argumentMap : [String: Any?] = [
"name": name,
"id": peripheralId,
"state": peripheralState
]
scannedPeripheralList.append(peripheral)
didDiscoverController.eventSink?(argumentMap)
}
}
// Called automatically when a peripheral is paired to your phone
func centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) {
if (event == .peerConnected) {
print("IOS: Case is peer connected")
connectToPeripheral(peripheral.identifier.uuidString)
} else if (event == .peerDisconnected ){
print("IOS: Peer %@ disconnected!", peripheral)
}
}
// Called automatically when connectToPeripheral() connects to a device
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
let name: String = peripheral.name ?? "No name"
let peripheralId: String = peripheral.identifier.uuidString
let argumentMap : [String: String] = [
"name": name,
"id": peripheralId
]
didConnectController.eventSink?(argumentMap)
}
// Called automatically when connectToPeripheral tries to connect to a device but fails
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
let errorMessage: String = error?.localizedDescription ?? ""
let name: String = peripheral.name ?? "No name"
let peripheralId: String = peripheral.identifier.uuidString
let argumentMap: [String: String] = [
"name" : name,
"id": peripheralId,
"error": errorMessage
]
didFailToConnectController.eventSink?(argumentMap)
}
// Called automatically when disconnectFromPeripheral() disconnects from a device
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
let name: String = peripheral.name ?? "No name"
let peripheralId: String = peripheral.identifier.uuidString
var argumentMap: [String: Any] = [
"name" : name,
"id": peripheralId
]
if let error = error {
argumentMap["error"] = error.localizedDescription
}
didDisconnectController.eventSink?(argumentMap)
}
// Called automatically when there is a change in the Bluetooth adapter's state.
func centralManagerDidUpdateState(_ central: CBCentralManager) {
let arguments : Int = central.state.rawValue
cbManagerStateController.eventSink?(arguments)
}
}
最后,控制器类列表,它们大部分都是相同的:
class CBManagerStateController: NSObject, FlutterStreamHandler {
var eventSink: FlutterEventSink?
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
}
class ConnectionEventDidOccurController: NSObject, FlutterStreamHandler {
var eventSink: FlutterEventSink?
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
}
// other controllers...
在 Flutter 方面,首先我在单独的文件中将通道声明为常量:
// Flutter sends requests, IOS returns a response
const MethodChannel bluetoothChannel = MethodChannel("bluetooth_channel");
// IOS sends values as streams through these channels
const EventChannel cbManagerStateChannel = EventChannel("cbmanager_state_channel");
// other channels...
接下来,我专门为我可以直接调用的方法创建了一个存储库类,并将其包装在提供程序中,以便于注入。它们大多相似,使用 try/catch 块来获取 PlatformExceptions 并打印从 IOS 端获得的值:
@riverpod
class BluetoothRepository extends _$BluetoothRepository {
@override
void build() {}
Future<int> getCBManagerState() async {
int initialState = 0;
initialState = await bluetoothChannel.invokeMethod<int>('getCBManagerState') as int;
debugPrint('Repository: initialCBManagerState arrived with value: $initialState');
return initialState;
}
Future<void> scanPeripherals() async {
await bluetoothChannel.invokeMethod('scanPeripherals');
}
// other methods, stop scan, connect/disconnect to/from peripheral, etc...
对于 CBManager 的状态,我使用两个单独的提供程序作为解决方法来获取初始值,并仅在值更改时接收流:
@riverpod
class InitialCBManagerState extends _$InitialCBManagerState {
@override
FutureOr<int> build() async {
FutureOr<int> initialCBManagerState = 0;
initialCBManagerState = await getCBManagerState();
return initialCBManagerState;
}
FutureOr<int> getCBManagerState() async {
int initialState = 0;
initialState = await ref.read(bluetoothRepositoryProvider.notifier).getCBManagerState();
debugPrint('Provider: initialCBManagerState arrived with value: $initialState');
return initialState;
}
}
@riverpod
Stream<int> cBManagerState(CBManagerStateRef ref, EventChannel channel) async* {
Stream<dynamic>? cbManagerStateStream;
int cbManagerState;
cbManagerStateStream = channel.receiveBroadcastStream();
await for (final state in cbManagerStateStream) {
debugPrint('Provider: new cbManagerState arrived with value: $state');
cbManagerState = state;
yield cbManagerState;
}
}
在另一个文件中,我创建了一个专注于扫描过程及其状态的提供程序:
@riverpod
class BluetoothScan extends _$BluetoothScan {
@override
bool build(MethodChannel channel) => false;
Future<void> _updateIsScanning() async {
bool? isScanning = await channel.invokeMethod('getIsScanning') as bool;
debugPrint('Provider: isScanning $isScanning');
state = isScanning;
}
Future<void> scanPeripherals() async {
try {
ref.read(bluetoothRepositoryProvider.notifier).scanPeripherals();
_updateIsScanning();
} catch (exception) {
debugPrint('Controller: Exception while scanning: $exception');
}
}
// Also stop scan...
}
最后,在我的最后一个提供程序文件中,我创建了专门用于:调用存储库方法、处理连接尝试和事件、检索已连接的设备和处理发现的提供程序:
@riverpod
class BluetoothController extends _$BluetoothController {
@override
void build() {}
Future<void> connectToPeripheral(String peripheralId) async {
ref.read(bluetoothRepositoryProvider.notifier).connectToPeripheral(peripheralId);
}
// other methods like disconnect...
}
@riverpod
Stream<List<BluetoothPeripheral>> connectionEventDidOccur(ConnectionEventDidOccurRef ref, EventChannel channel) async* {
debugPrint('Provider: connectionEventDidOccur was called');
Stream<dynamic> connectionEventDidOccurStream;
BluetoothPeripheral peripheral;
List<BluetoothPeripheral> peripheralList = [];
CBPeripheralState peripheralState;
connectionEventDidOccurStream = channel.receiveBroadcastStream();
await for (final device in connectionEventDidOccurStream) {
debugPrint('Provider: connectionEventDidOccur device in for loop is $device');
peripheralState = ref.read(parsedPeripheralStateProvider(device['state']));
BluetoothPeripheral bluetoothPeripheral = BluetoothPeripheral.fromJson({
'name': device['name'],
'id': device['id'],
'state': peripheralState.name,
});
if (!peripheralList.contains(bluetoothPeripheral)) {
debugPrint('Provider: didDiscover if statement condition fulfilled');
peripheral = bluetoothPeripheral;
debugPrint('Provider: didDiscover peripheral before adding to list is ${peripheral.name}, ${peripheral.id}');
peripheralList.add(bluetoothPeripheral);
debugPrint(
'Provider: didDiscover peripheralList before yield length is ${peripheralList.length}, and content is ${peripheralList.toString()}');
yield peripheralList;
}
}
}
@riverpod
Future<List<BluetoothPeripheral>> connectedPeripherals(ConnectedPeripheralsRef ref, MethodChannel channel) async {
ref.watch(connectionEventDidOccurProvider(connectionEventDidOccurChannel));
List<Map<String, String>>? deviceMapList = [];
List<BluetoothPeripheral> bluetoothPeripheralList = [];
deviceMapList = await channel.invokeListMethod<Map<String, String>>('getConnectedPeripherals');
debugPrint('Provider: deviceMapList $deviceMapList');
for (final device in deviceMapList!) {
final bluetoothPeripheral = BluetoothPeripheral.fromJson(device);
debugPrint('Provider: deviceMapList $bluetoothPeripheral');
bluetoothPeripheralList.add(bluetoothPeripheral);
}
return bluetoothPeripheralList;
}
// other providers...
转向 UI,我本质上是使用 Riverpod 中的provider.when 语法来根据每个提供程序的状态显示不同的数据。
另外这就是第一个大错误发生的地方,即使我已经连接到设备,列表始终是空的,从IOS端一直到视图的所有打印都证实了这一点:
class BluetoothConfiguration extends ConsumerWidget {
const BluetoothConfiguration({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<List<BluetoothPeripheral>> peripheralList = ref.watch(connectedPeripheralsProvider(bluetoothChannel));
final AsyncValue<int> cbManagerProvider = ref.watch(cBManagerStateProvider(cbManagerStateChannel));
// other providers...
return Scaffold(
body: Padding(
child: Center(
child: Column(
children: [
peripheralList.when(
data: (peripheral) {
if (peripheral.isNotEmpty) {
return Column(
children: [
const Text('Choose your main device'),
ListView.builder(itemBuilder: (context, index) {
return ListTile(
title: Text(peripheral[index].name),
subtitle: Text(peripheral[index].id),
trailing: IconButton(
onPressed: () {
peripheral[index] = peripheral[index].copyWith(mainPeripheral: true);
localStorage.value?.setString('mainPeripheral', peripheral[index].id);
},
icon: peripheral[index].id == localStorage.value?.getString('mainPeripheral')
? const Icon(Icons.play_circle_fill)
: const Icon(Icons.play_circle)));
}),
],
);
} else {
return const Column(
children: [
Text('There are no connected devices'),
SizedBox(height: 10),
Text('Connect your first one below'),
],
);
}
},
error: (_, stackTrace) => const Icon(Icons.error),
loading: () => const CircularProgressIndicator(),
),
const SizedBox(height: 40),
cbManagerProvider.when(
data: (cbManagerState) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () async {
(cbManagerState == 5 && !scanProvider)
? await ref.read(bluetoothScanProvider(bluetoothChannel).notifier).scanPeripherals()
: null;
context.mounted
? showDialog(
context: context,
builder: (context) => const DeviceListDialog(),
)
: null;
},
child: Text(cbManagerState == 5 && !scanProvider ? 'Scan Devices' : 'Cannot Scan'),
),
],
),
error: (_, stackTrace) => Column(
children: [
const Icon(Icons.bluetooth_disabled),
Text('Error of type: ${stackTrace.toString()}'),
],
),
loading: () => initialCBManagerProvider.when(
data: (initialCBManagerState) => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () async {
(initialCBManagerState == 5 && !scanProvider)
? await ref.read(bluetoothScanProvider(bluetoothChannel).notifier).scanPeripherals()
: null;
context.mounted
? showDialog(
context: context,
builder: (context) => const DeviceListDialog(),
)
: null;
},
child: Text(initialCBManagerState == 5 && !scanProvider ? 'Scan Devices' : 'Cannot Scan'),
),
],
),
error: (_, stackTrace) => Column(
children: [
const Icon(Icons.bluetooth_disabled),
Text('Error of type: ${stackTrace.toString()}'),
],
),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
],
),
)),
);
}
}
终于出现了一个对话框,显示当前正在扫描的设备,这是第二个大错误发生的地方,因为我根本没有找到周围有很多设备(尽管我还有很多其他设备),即使我在扫描,打开蓝牙,它们处于广告模式。
找到的设备完美地呈现在屏幕上,并带有名称和 ID。
class DeviceListDialog extends ConsumerStatefulWidget {
const DeviceListDialog({super.key});
@override
ConsumerState<DeviceListDialog> createState() => _DeviceListDialogState();
}
class _DeviceListDialogState extends ConsumerState<DeviceListDialog> {
void _closeModal() {
ref.read(bluetoothScanProvider(bluetoothChannel).notifier).stopScan();
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
final AsyncValue<List<BluetoothPeripheral>> discoveredPeripheralStream = ref.watch(didDiscoverProvider(didDiscoverChannel));
return AlertDialog(
title: Row(
children: [
const Text(
'Available Devices',
style: TextStyle(color: SparxColor.secondaryTextColor),
),
IconButton(
onPressed: _closeModal,
icon: const Icon(Icons.clear),
),
],
),
content: SingleChildScrollView(
child: SizedBox(
height: 300,
width: 250,
child: switch (discoveredPeripheralStream) {
AsyncData(:var value) => ListView.builder(
itemCount: value.length,
itemBuilder: ((context, index) {
debugPrint('View: Peripheral Map in Dialog is ${value[index]}');
return ListTile(
title: Text(value[index].name),
subtitle: Text(value[index].id),
trailing: ElevatedButton(
onPressed: () {
value[index].state != CBPeripheralState.connected
? ref.read(bluetoothControllerProvider.notifier).connectToPeripheral(value[index].id)
: null;
},
child: value[index].state == CBPeripheralState.connecting
? const CircularProgressIndicator()
: value[index].state == CBPeripheralState.connected
? const Text('Connected', selectionColor: SparxColor.tertiaryColor)
: const Text('Connect')),
);
})),
AsyncError(:final error) => ErrorWidget(error),
_ => const Row(mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()]),
},
),
),
actions: [
TextButton(
onPressed: _closeModal,
child: const Text('Stop Scan'),
),
],
);
}
}
就是这样,感谢您阅读本文。
iOS 13 对核心蓝牙的更改不会让您的应用程序扫描所有 BR/EDR 设备。
核心蓝牙可以与支持 GATT 配置文件的 BR/EDR 外设交互 - 这与核心蓝牙始终支持的配置文件相同,但之前仅支持 BLE。
核心蓝牙不允许您的应用程序发现不支持 GATT 配置文件的 BR/EDR 外围设备或与之交互。
例如,典型的汽车音频系统将支持 A2DP、AVRCP 和 HFP 配置文件,但不支持 BLE 或 BR/EDR 上的 GATT 配置文件。
详细介绍了其工作原理从视频中的这一点开始
另请注意,这部分文字记录:
那么,从您的应用程序的角度来看,传入连接是什么样的?您的应用程序将实例化一个 CBCentralManager,向我们传递一个已知的服务 UID,如果是 BR/EDR 或经典设备,您的用户将转到蓝牙设置并搜索该设备,在本例中假设它是耳机跑步心率。他们会发现该设备,找到它并尝试连接。配对将被触发,然后当我们连接时,我们将运行 GATT 服务的服务发现。如果我们找到您想要的服务,那么您将收到委托回调。
了解用户仍然必须如何从蓝牙设置启动与经典设备的连接。只有在经典配对发生后,如果设备公开了应用程序已注册的 GATT 服务,您的应用程序才会收到通知;本例中的心率服务。