Flutter蓝牙通信插件flutter_blue_pro的使用

Flutter蓝牙通信插件flutter_blue_pro的使用

引言

FlutterBluePlus 是一个为 Flutter 开发的蓝牙插件,用于帮助开发者构建现代跨平台应用。注意:该插件是从 FlutterBlue 持续开发而来,因为 FlutterBlue 的维护已经停止。

Alpha 版本

此插件必须在真实设备上进行测试。

跨平台蓝牙低功耗 (BLE)

FlutterBluePlus 目标是在两个平台(iOS 和 Android)上提供最佳功能。

通过 FlutterBluePlus 实例,您可以扫描并连接到附近的设备(BluetoothDevice)。一旦连接到设备,BluetoothDevice 对象可以发现服务(BluetoothService)、特征(BluetoothCharacteristic)和描述符(BluetoothDescriptor)。然后可以使用 BluetoothDevice 对象直接与特征和描述符进行交互。

使用

获取实例

FlutterBluePlus flutterBlue = FlutterBluePlus.instance;

扫描设备

// 开始扫描
flutterBlue.startScan(timeout: Duration(seconds: 4));

// 监听扫描结果
var subscription = flutterBlue.scanResults.listen((results) {
    // 处理扫描结果
    for (ScanResult r in results) {
        print('${r.device.name} found! rssi: ${r.rssi}');
    }
});

// 停止扫描
flutterBlue.stopScan();

连接到设备

// 连接到设备
await device.connect();

// 断开设备连接
device.disconnect();

发现服务

List<BluetoothService> services = await device.discoverServices();
services.forEach((service) {
    // 处理服务
});

读取和写入特征

// 读取所有特征
var characteristics = service.characteristics;
for(BluetoothCharacteristic c in characteristics) {
    List<int> value = await c.read();
    print(value);
}

// 向特征写入数据
await c.write([0x12, 0x34]);

读取和写入描述符

// 读取所有描述符
var descriptors = characteristic.descriptors;
for(BluetoothDescriptor d in descriptors) {
    List<int> value = await d.read();
    print(value);
}

// 向描述符写入数据
await d.write([0x12, 0x34]);

设置通知并监听变化

await characteristic.setNotifyValue(true);
characteristic.value.listen((value) {
    // 处理新值
});

读取 MTU 并请求更大的尺寸

final mtu = await device.mtu.first;
await device.requestMtu(512);

注意:iOS 不允许请求 MTU 大小,并将始终尝试协商最高的可能 MTU(iOS 支持高达 185 字节的 MTU)。

请求连接优先级

await device.requestConnectionPriority(connectionPriorityRequest: ConnectionPriority.high);

仅在 Android 中可以向 BLE 设备发送连接优先级更新。参数 priority 是一个枚举,使用与 BluetoothGatt Android 规范相同的规范。使用高性能会增加电池消耗但会加速 GATT 操作。当与多个设备通信时要谨慎设置优先级,因为如果为所有设备设置高性能效果会降低。

入门指南

更改 Android 的 minSdkVersion

flutter_blue_plus 仅兼容从 Android SDK 版本 19 开始的版本,因此需要在 android/app/build.gradle 中更改:

Android {
  defaultConfig {
     minSdkVersion: 19

添加蓝牙权限

我们需要添加使用蓝牙和访问位置的权限:

Android

在 android/app/src/main/AndroidManifest.xml 中添加:

	 <uses-permission android:name="android.permission.BLUETOOTH" />  
	 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />  
	 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
 <application

iOS

在 ios/Runner/Info.plist 中添加:

	<dict>  
	    <key>NSBluetoothAlwaysUsageDescription</key>  
	    <string>Need BLE permission</string>  
	    <key>NSBluetoothPeripheralUsageDescription</key>  
	    <string>Need BLE permission</string>  
	    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>  
	    <string>Need Location permission</string>  
	    <key>NSLocationAlwaysUsageDescription</key>  
	    <string>Need Location permission</string>  
	    <key>NSLocationWhenInUseUsageDescription</key>  
	    <string>Need Location permission</string>

对于 iOS 上的位置权限,请参阅:Apple Developer Documentation

参考

FlutterBlue API

功能 Android iOS 描述
扫描 ✅ ✅ 开始扫描 BLE 设备
状态 ✅ ✅ 蓝牙适配器状态流
是否可用 ✅ ✅ 检查设备是否支持蓝牙
是否开启 ✅ ✅ 检查蓝牙功能是否开启

BluetoothDevice API

功能 Android iOS 描述
连接 ✅ ✅ 建立与设备的连接
断开连接 ✅ ✅ 取消与设备的连接
发现服务 ✅ ✅ 发现远程设备提供的服务及其特征和描述符
服务 ✅ ✅ 获取服务列表。需要完成 discoverServices()
状态 ✅ ✅ 蓝牙设备状态流
MTU ✅ ✅ MTU 大小流
请求 MTU ✅ ✅ 请求更改设备的 MTU
读取 RSSI ✅ ✅ 从连接设备读取 RSSI
请求连接优先级 ✅ ✅ 请求更新高优先级、低延迟连接

BluetoothCharacteristic API

功能 Android iOS 描述
读取 ✅ ✅ 读取特征的值
写入 ✅ ✅ 写入特征的值
设置通知 ✅ ✅ 在特征上设置通知或指示
值 ✅ ✅ 特征值变化流

BluetoothDescriptor API

功能 Android iOS 描述
读取 ✅ ✅ 读取描述符的值
写入 ✅ ✅ 写入描述符的值

故障排除

当我使用服务 UUID 过滤器扫描时,找不到任何设备。

确保设备正在广播其支持的服务 UUID。这可以在广告包中找到,作为“UUID 16 位完整列表”或“UUID 128 位完整列表”。

示例代码

以下是一个完整的示例代码,展示了如何使用 flutter_blue_plus 插件进行蓝牙通信。

// Copyright 2017, Paul DeMarco.
// All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_blue_pro/flutter_blue_pro.dart';

import 'widgets.dart';

void main() {
  runApp(const FlutterBlueApp());
}

class FlutterBlueApp extends StatelessWidget {
  const FlutterBlueApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      color: Colors.lightBlue,
      home: StreamBuilder<BluetoothState>(
          stream: FlutterBluePlus.instance.state,
          initialData: BluetoothState.unknown,
          builder: (c, snapshot) {
            final state = snapshot.data;
            if (state == BluetoothState.on) {
              return const FindDevicesScreen();
            }
            return BluetoothOffScreen(state: state);
          }),
    );
  }
}

class BluetoothOffScreen extends StatelessWidget {
  const BluetoothOffScreen({Key? key, this.state}) : super(key: key);

  final BluetoothState? state;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.lightBlue,
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            const Icon(
              Icons.bluetooth_disabled,
              size: 200.0,
              color: Colors.white54,
            ),
            Text(
              'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.',
              style: Theme.of(context)
                  .primaryTextTheme
                  .subtitle2
                  ?.copyWith(color: Colors.white),
            ),
            ElevatedButton(
              child: const Text('TURN ON'),
              onPressed: Platform.isAndroid
                  ? () => FlutterBluePlus.instance.turnOn()
                  : null,
            ),
          ],
        ),
      ),
    );
  }
}

class FindDevicesScreen extends StatelessWidget {
  const FindDevicesScreen({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Find Devices'),
        actions: [
          ElevatedButton(
            child: const Text('TURN OFF'),
            style: ElevatedButton.styleFrom(
              primary: Colors.black,
              onPrimary: Colors.white,
            ),
            onPressed: Platform.isAndroid
                ? () => FlutterBluePlus.instance.turnOff()
                : null,
          ),
        ],
      ),
      body: RefreshIndicator(
        onRefresh: () => FlutterBluePlus.instance
            .startScan(timeout: const Duration(seconds: 4)),
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              StreamBuilder<List<BluetoothDevice>>(
                stream: Stream.periodic(const Duration(seconds: 2))
                    .asyncMap((_) => FlutterBluePlus.instance.connectedDevices),
                initialData: const [],
                builder: (c, snapshot) => Column(
                  children: snapshot.data!
                      .map((d) => ListTile(
                            title: Text(d.name),
                            subtitle: Text(d.id.toString()),
                            trailing: StreamBuilder<BluetoothDeviceState>(
                              stream: d.state,
                              initialData: BluetoothDeviceState.disconnected,
                              builder: (c, snapshot) {
                                if (snapshot.data == BluetoothDeviceState.connected) {
                                  return ElevatedButton(
                                    child: const Text('OPEN'),
                                    onPressed: () => Navigator.of(context).push(
                                        MaterialPageRoute(
                                            builder: (context) => DeviceScreen(device: d))),
                                  );
                                }
                                return Text(snapshot.data.toString());
                              },
                            ),
                          ))
                      .toList(),
                ),
              ),
              StreamBuilder<List<ScanResult>>(
                stream: FlutterBluePlus.instance.scanResults,
                initialData: const [],
                builder: (c, snapshot) => Column(
                  children: snapshot.data!
                      .map(
                        (r) => ScanResultTile(
                          result: r,
                          onTap: () => Navigator.of(context)
                              .push(MaterialPageRoute(builder: (context) {
                            r.device.connect();
                            return DeviceScreen(device: r.device);
                          })),
                        ),
                      )
                      .toList(),
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: StreamBuilder<bool>(
        stream: FlutterBluePlus.instance.isScanning,
        initialData: false,
        builder: (c, snapshot) {
          if (snapshot.data!) {
            return FloatingActionButton(
              child: const Icon(Icons.stop),
              onPressed: () => FlutterBluePlus.instance.stopScan(),
              backgroundColor: Colors.red,
            );
          } else {
            return FloatingActionButton(
                child: const Icon(Icons.search),
                onPressed: () => FlutterBluePlus.instance
                    .startScan(timeout: const Duration(seconds: 4)));
          }
        },
      ),
    );
  }
}

class DeviceScreen extends StatefulWidget {
  const DeviceScreen({Key? key, required this.device}) : super(key: key);

  final BluetoothDevice device;

  [@override](/user/override)
  State<DeviceScreen> createState() => _DeviceScreenState();
}

class _DeviceScreenState extends State<DeviceScreen> {
  ConnectionPriority? connectionPriority;

  List<int> _getRandomBytes() {
    final math = Random();
    return [
      math.nextInt(255),
      math.nextInt(255),
      math.nextInt(255),
      math.nextInt(255)
    ];
  }

  List<Widget> _buildServiceTiles(List<BluetoothService> services) {
    return services
        .map(
          (s) => ServiceTile(
            service: s,
            characteristicTiles: s.characteristics
                .map(
                  (c) => CharacteristicTile(
                    characteristic: c,
                    onReadPressed: () => c.read(),
                    onWritePressed: () async {
                      await c.write(_getRandomBytes(), withoutResponse: true);
                      await c.read();
                    },
                    onNotificationPressed: () async {
                      await c.setNotifyValue(!c.isNotifying);
                      await c.read();
                    },
                    descriptorTiles: c.descriptors
                        .map(
                          (d) => DescriptorTile(
                            descriptor: d,
                            onReadPressed: () => d.read(),
                            onWritePressed: () => d.write(_getRandomBytes()),
                          ),
                        )
                        .toList(),
                  ),
                )
                .toList(),
          ),
        )
        .toList();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.device.name),
        actions: <Widget>[
          StreamBuilder<BluetoothDeviceState>(
            stream: widget.device.state,
            initialData: BluetoothDeviceState.connecting,
            builder: (c, snapshot) {
              VoidCallback? onPressed;
              String text;
              switch (snapshot.data) {
                case BluetoothDeviceState.connected:
                  onPressed = () => widget.device.disconnect();
                  text = 'DISCONNECT';
                  break;
                case BluetoothDeviceState.disconnected:
                  onPressed = () => widget.device.connect();
                  text = 'CONNECT';
                  break;
                default:
                  onPressed = null;
                  text = snapshot.data.toString().substring(21).toUpperCase();
                  break;
              }
              return TextButton(
                  onPressed: onPressed,
                  child: Text(
                    text,
                    style: Theme.of(context)
                        .primaryTextTheme
                        .button
                        ?.copyWith(color: Colors.white),
                  ));
            },
          )
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            StreamBuilder<BluetoothDeviceState>(
              stream: widget.device.state,
              initialData: BluetoothDeviceState.connecting,
              builder: (c, snapshot) => ListTile(
                leading: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    snapshot.data == BluetoothDeviceState.connected
                        ? const Icon(Icons.bluetooth_connected)
                        : const Icon(Icons.bluetooth_disabled),
                    snapshot.data == BluetoothDeviceState.connected
                        ? StreamBuilder<int>(
                            stream: rssiStream(),
                            builder: (context, snapshot) {
                              return Text(
                                  snapshot.hasData ? '${snapshot.data}dBm' : '',
                                  style: Theme.of(context).textTheme.caption);
                            })
                        : Text('', style: Theme.of(context).textTheme.caption),
                  ],
                ),
                title: Text(
                    'Device is ${snapshot.data.toString().split('.')[1]}.'),
                subtitle: Text('${widget.device.id}'),
                trailing: StreamBuilder<bool>(
                  stream: widget.device.isDiscoveringServices,
                  initialData: false,
                  builder: (c, snapshot) => IndexedStack(
                    index: snapshot.data! ? 1 : 0,
                    children: <Widget>[
                      IconButton(
                        icon: const Icon(Icons.refresh),
                        onPressed: () => widget.device.discoverServices(),
                      ),
                      const IconButton(
                        icon: SizedBox(
                          child: CircularProgressIndicator(
                            valueColor: AlwaysStoppedAnimation(Colors.grey),
                          ),
                          width: 18.0,
                          height: 18.0,
                        ),
                        onPressed: null,
                      )
                    ],
                  ),
                ),
              ),
            ),
            StreamBuilder<int>(
              stream: widget.device.mtu,
              initialData: 0,
              builder: (c, snapshot) => ListTile(
                title: const Text('MTU Size'),
                subtitle: Text('${snapshot.data} bytes'),
                trailing: IconButton(
                  icon: const Icon(Icons.edit),
                  onPressed: () => widget.device.requestMtu(223),
                ),
              ),
            ),
            ListTile(
              onTap: () async {
                showDialog(
                    context: context,
                    builder: ((context) {
                      return Dialog(
                        alignment: Alignment.center,
                        child: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const ListTile(
                              title:
                                  Text('Choose connection parameter update:'),
                            ),
                            ListTile(
                              title: const Text('Connection Priority Balanced'),
                              trailing: IconButton(
                                icon: const Icon(Icons.edit),
                                onPressed: () async => await widget.device
                                    .requestConnectionPriority(
                                  connectionPriorityRequest:
                                      ConnectionPriority.balanced,
                                )
                                    .whenComplete(() {
                                  connectionPriority =
                                      ConnectionPriority.balanced;
                                  setState(() {});

                                  Navigator.pop(context);
                                }),
                              ),
                            ),
                            ListTile(
                              title: const Text('Connection Priority High'),
                              trailing: IconButton(
                                icon: const Icon(Icons.edit),
                                onPressed: () async => await widget.device
                                    .requestConnectionPriority(
                                  connectionPriorityRequest:
                                      ConnectionPriority.high,
                                )
                                    .whenComplete(() {
                                  connectionPriority = ConnectionPriority.high;
                                  setState(() {});

                                  Navigator.pop(context);
                                }),
                              ),
                            ),
                            ListTile(
                              title:
                                  const Text('Connection Priority Low Power'),
                              trailing: IconButton(
                                icon: const Icon(Icons.edit),
                                onPressed: () async => await widget.device
                                    .requestConnectionPriority(
                                  connectionPriorityRequest:
                                      ConnectionPriority.lowPower,
                                )
                                    .whenComplete(() {
                                  connectionPriority =
                                      ConnectionPriority.lowPower;
                                  setState(() {});
                                  Navigator.pop(context);
                                }),
                              ),
                            ),
                          ],
                        ),
                      );
                    }));
              },
              title: const Text(' Request Connection Priority'),
              subtitle: connectionPriority != null
                  ? Text('Connection priority status: $connectionPriority')
                  : null,
              trailing: const Icon(Icons.connect_without_contact),
            ),
            StreamBuilder<List<BluetoothService>>(
              stream: widget.device.services,
              initialData: const [],
              builder: (c, snapshot) {
                return Column(
                  children: _buildServiceTiles(snapshot.data!),
                );
              },
            ),
          ],
        ),
      ),
    );
  }

  Stream<int> rssiStream() async* {
    var isConnected = true;
    final subscription = widget.device.state.listen((state) {
      isConnected = state == BluetoothDeviceState.connected;
    });
    while (isConnected) {
      yield await widget.device.readRssi();
      await Future.delayed(const Duration(seconds: 1));
    }
    subscription.cancel();
    // 设备断开连接,停止 RSSI 流
  }
}

更多关于Flutter蓝牙通信插件flutter_blue_pro的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter蓝牙通信插件flutter_blue_pro的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


flutter_blue_pro 是一个用于在 Flutter 应用中实现蓝牙通信的插件。它支持 Android 和 iOS 平台,并提供了丰富的 API 来扫描、连接、发现服务和特征值,以及进行数据的读写操作。

以下是如何使用 flutter_blue_pro 插件进行蓝牙通信的基本步骤:

1. 添加依赖

首先,在 pubspec.yaml 文件中添加 flutter_blue_pro 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_blue_pro: ^0.0.1 # 请使用最新版本

然后运行 flutter pub get 来获取依赖。

2. 初始化 FlutterBluePro

在你的 Dart 文件中,导入 flutter_blue_pro 并初始化 FlutterBluePro 实例:

import 'package:flutter_blue_pro/flutter_blue_pro.dart';

final FlutterBluePro flutterBlue = FlutterBluePro.instance;

3. 扫描蓝牙设备

使用 startScan 方法来扫描附近的蓝牙设备:

void startScan() {
  flutterBlue.startScan(
    timeout: Duration(seconds: 4),
    allowDuplicates: false,
  );

  flutterBlue.scanResults.listen((results) {
    for (ScanResult result in results) {
      print('Found device: ${result.device.name}');
    }
  });
}

4. 停止扫描

使用 stopScan 方法来停止扫描:

void stopScan() {
  flutterBlue.stopScan();
}

5. 连接设备

使用 connect 方法来连接到一个蓝牙设备:

void connectToDevice(BluetoothDevice device) async {
  await device.connect();
  print('Connected to ${device.name}');
}

6. 发现服务和特征值

连接成功后,你可以发现设备提供的服务和特征值:

void discoverServices(BluetoothDevice device) async {
  List<BluetoothService> services = await device.discoverServices();
  for (BluetoothService service in services) {
    print('Service: ${service.uuid}');
    for (BluetoothCharacteristic characteristic in service.characteristics) {
      print('Characteristic: ${characteristic.uuid}');
    }
  }
}

7. 读取和写入特征值

你可以使用 read 和 write 方法来读取和写入特征值:

void readCharacteristic(BluetoothCharacteristic characteristic) async {
  List<int> value = await characteristic.read();
  print('Characteristic value: $value');
}

void writeCharacteristic(BluetoothCharacteristic characteristic, List<int> value) async {
  await characteristic.write(value);
  print('Characteristic value written');
}

8. 断开连接

使用 disconnect 方法来断开与设备的连接:

void disconnectDevice(BluetoothDevice device) async {
  await device.disconnect();
  print('Disconnected from ${device.name}');
}

9. 处理状态变化

你可以监听设备连接状态的变化:

void listenToConnectionState(BluetoothDevice device) {
  device.connectionState.listen((state) {
    print('Connection state: $state');
  });
}

10. 权限处理

在 Android 和 iOS 上,蓝牙操作需要相应的权限。确保在 AndroidManifest.xml 和 Info.plist 中添加必要的权限配置。

Android:

在 AndroidManifest.xml 中添加以下权限:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

iOS:

在 Info.plist 中添加以下键值对:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>We need access to Bluetooth for connecting to your device.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>We need access to Bluetooth for connecting to your device.</string>
回到顶部