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 流
  }
}
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. 读取和写入特征值

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

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.xmlInfo.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>
回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!