Flutter 应用程序无法使用 IOS/Swift 的 BluetoothCore lib 发现某些外围设备

问题描述 投票:0回答:1

我正在开发 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 进行测试。

然而,即使尝试复制文档中的步骤,我也根本无法:

  1. 在扫描过程中查找汽车、手表等设备
  2. 显示已连接设备的列表

我知道诸如“设备必须处于广告模式”之类的事情。

请记住,我无法与设备提供商、制造商等联系以获得任何类型的 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 swift flutter bluetooth bluetooth-gatt
1个回答
0
投票

iOS 13 对核心蓝牙的更改不会让您的应用程序扫描所有 BR/EDR 设备。

核心蓝牙可以与支持 GATT 配置文件的 BR/EDR 外设交互 - 这与核心蓝牙始终支持的配置文件相同,但之前仅支持 BLE。

核心蓝牙不允许您的应用程序发现不支持 GATT 配置文件的 BR/EDR 外围设备或与之交互。

例如,典型的汽车音频系统将支持 A2DP、AVRCP 和 HFP 配置文件,但不支持 BLE 或 BR/EDR 上的 GATT 配置文件。

详细介绍了其工作原理从视频中的这一点开始

另请注意,这部分文字记录:

那么,从您的应用程序的角度来看,传入连接是什么样的?您的应用程序将实例化一个 CBCentralManager,向我们传递一个已知的服务 UID,如果是 BR/EDR 或经典设备,您的用户将转到蓝牙设置并搜索该设备,在本例中假设它是耳机跑步心率。他们会发现该设备,找到它并尝试连接。配对将被触发,然后当我们连接时,我们将运行 GATT 服务的服务发现。如果我们找到您想要的服务,那么您将收到委托回调。

了解用户仍然必须如何从蓝牙设置启动与经典设备的连接。只有在经典配对发生后,如果设备公开了应用程序已注册的 GATT 服务,您的应用程序才会收到通知;本例中的心率服务。

© www.soinside.com 2019 - 2024. All rights reserved.