Flutter蓝牙通信插件flutter_bluex的使用

Flutter蓝牙通信插件flutter_bluex的使用

这是一个 fork 版本,使用前请注意!

由于原先的 flutter_blue 不再维护,我 fork 了原仓库并针对新版本 Flutter 继续维护:

  • 增加了获取已绑定设备(已配对设备)的方法,已配对设备可以不需要扫描就能连接(仅安卓)。
  • 增加了更直观的直接获取设备连接状态和 MTU 的函数,而不是从流获得第一个状态。
  • 修复了在高版本安卓上由于一个无用的 UUID 类导致的闪退问题。
  • 修复了原来的 connect 方法中的 timeout 设置并不会在当前的 await 流程中抛出异常的问题。
  • 修复了示例在高版本安卓上运行的问题。

Introduction

flutter_blue 是一个为 Flutter 设计的蓝牙插件,帮助开发者构建现代跨平台应用。

Alpha version

此库正在与生产应用一起积极开发,API 将随着我们向版本 1.0 的发展而演变。

请充分准备应对可能的破坏性更改。 此包必须在真实设备上进行测试。

如果遇到适应最新 API 的困难,请联系我,我很乐意听取您的用例。

Cross-Platform Bluetooth LE

flutter_blue 目标是提供两个平台(iOS 和 Android)的最佳功能。

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

Usage

Obtain an instance

FlutterBlue flutterBlue = FlutterBlue.instance;

Scan for devices

// 开始扫描
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();

Connect to a device

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

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

Discover services

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

Read and write characteristics

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

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

Read and write descriptors

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

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

Set notifications and listen to changes

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

Read the MTU and request larger size

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

注意:iOS 不允许请求 MTU 大小,并且总是尝试协商最大的可能 MTU(iOS 支持高达 MTU 大小 185)

Getting Started

Change the minSdkVersion for Android

flutter_blue 仅从 Android SDK 版本 19 兼容,因此您应该在 android/app/build.gradle 中更改此设置:

Android {
  defaultConfig {
     minSdkVersion: 19

Add permissions for Bluetooth

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

Android

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

    <!-- Request legacy Bluetooth permissions on older devices. -->
    <uses-permission android:name="android.permission.BLUETOOTH"
                     android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
                     android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    <!-- Needed only if your app looks for Bluetooth devices.
         You must add an attribute to this permission, or declare the
         ACCESS_FINE_LOCATION permission, depending on the results when you
         check location usage in your app. -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

    <!-- Needed only if your app makes the device discoverable to Bluetooth
         devices. -->
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

    <!-- Needed only if your app communicates with already-paired Bluetooth
         devices. -->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

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 的位置权限,请参阅:https://developer.apple.com/documentation/corelocation/requesting_authorization_for_location_services

Reference

FlutterBlue API

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

BluetoothDevice API

功能 Android iOS 描述
连接 ✅ ✅ 与设备建立连接
断开连接 ✅ ✅ 取消设备的活动或待定连接
发现服务 ✅ ✅ 发现远程设备提供的服务及其特征和描述符
服务 ✅ ✅ 获取服务列表。需要调用 discoverServices() 完成
状态 ✅ ✅ 蓝牙设备状态变化的流
MTU ✅ ✅ MTU 大小变化的流
请求 MTU ✅ ✅ 请求更改设备的 MTU

BluetoothCharacteristic API

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

BluetoothDescriptor API

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

Troubleshooting

When I scan using a service UUID filter, it doesn’t find any devices.

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

示例代码

// 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:math';

import 'package:flutter/material.dart';
import 'package:flutter_bluex/flutter_blue.dart';
import 'package:flutter_blue_example/widgets.dart';

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

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

/// 用于测试绑定设备的连接断开
class BondedTestPage extends StatefulWidget {
  const BondedTestPage({Key? key}) : super(key: key);

  [@override](/user/override)
  State<BondedTestPage> createState() => _BondedTestPageState();
}

class _BondedTestPageState extends State<BondedTestPage> {
  List<BluetoothDevice> deviceList = [];
  Map<String, BluetoothDeviceState> stateMap = {};

  /// 加载已绑定设备
  reloadBondedDevices() async {
    final res = await FlutterBlue.instance.bondedDevices;
    for (var eachDevice in res) {
      final state = await eachDevice.currentState();
      stateMap[eachDevice.id.id] = state;
    }
    setState(() {
      deviceList = res;
    });
  }

  [@override](/user/override)
  void initState() {
    super.initState();
    reloadBondedDevices();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bonded Device Test'),
        actions: [
          IconButton(
            onPressed: reloadBondedDevices,
            icon: Icon(Icons.refresh),
          )
        ],
      ),
      body: ListView(
        children: [
          for (final device in deviceList)
            Container(
              padding: EdgeInsets.symmetric(
                horizontal: 12,
                vertical: 12,
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('${device.name} ${device.id.id}'),
                      Text(
                        stateMap[device.id.id]?.toString() ?? "--",
                        style: TextStyle(
                          color: Colors.black.withOpacity(0.5),
                        ),
                      ),
                    ],
                  ),
                  Container(
                    child: Row(
                      children: [
                        TextButton(
                          child: Text('连接'),
                          onPressed: () async {
                            final time = DateTime.now();
                            await device.connect();
                            print(
                              'Connect Time Cost:'
                              '${DateTime.now().difference(time).inMilliseconds}ms',
                            );
                            await device.discoverServices();
                            print(
                              'DiscoverServices Time Cost:'
                              '${DateTime.now().difference(time).inMilliseconds}ms',
                            );
                            await reloadBondedDevices();
                          },
                        ),
                        TextButton(
                          child: Text('断开'),
                          onPressed: () async {
                            await device.disconnect();
                            await reloadBondedDevices();
                          },
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            )
        ],
      ),
    );
  }
}

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>[
            Icon(
              Icons.bluetooth_disabled,
              size: 200.0,
              color: Colors.white54,
            ),
            Text(
              'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.',
            ),
          ],
        ),
      ),
    );
  }
}

class FindDevicesScreen extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Find Devices'),
      ),
      body: RefreshIndicator(
        onRefresh: () => FlutterBlue.instance.startScan(timeout: Duration(seconds: 4)),
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              StreamBuilder<List<BluetoothDevice>>(
                stream: Stream.periodic(Duration(seconds: 2)).asyncMap((_) => FlutterBlue.instance.connectedDevices),
                initialData: [],
                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 TextButton(
                                    child: Text('OPEN'),
                                    onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => DeviceScreen(device: d))),
                                  );
                                }
                                return Text(snapshot.data.toString());
                              },
                            ),
                          ))
                      .toList(),
                ),
              ),
              StreamBuilder<List<ScanResult>>(
                stream: FlutterBlue.instance.scanResults,
                initialData: [],
                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: FlutterBlue.instance.isScanning,
        initialData: false,
        builder: (c, snapshot) {
          if (snapshot.data!) {
            return FloatingActionButton(
              child: Icon(Icons.stop),
              onPressed: () => FlutterBlue.instance.stopScan(),
              backgroundColor: Colors.red,
            );
          } else {
            return FloatingActionButton(
                child: Icon(Icons.search),
                onPressed: () => FlutterBlue.instance
                    .startScan(timeout: Duration(seconds: 4)));
          }
        },
      ),
    );
  }
}

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

  final BluetoothDevice device;

  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(device.name),
        actions: <Widget>[
          StreamBuilder<BluetoothDeviceState>(
            stream: device.state,
            initialData: BluetoothDeviceState.connecting,
            builder: (c, snapshot) {
              VoidCallback? onPressed;
              String text;
              switch (snapshot.data) {
                case BluetoothDeviceState.connected:
                  onPressed = () => device.disconnect();
                  text = 'DISCONNECT';
                  break;
                case BluetoothDeviceState.disconnected:
                  onPressed = () => device.connect();
                  text = 'CONNECT';
                  break;
                default:
                  onPressed = null;
                  text = snapshot.data.toString().substring(21).toUpperCase();
                  break;
              }
              return TextButton(
                  onPressed: onPressed,
                  child: Text(
                    text,
                  ));
            },
          )
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            StreamBuilder<BluetoothDeviceState>(
              stream: device.state,
              initialData: BluetoothDeviceState.connecting,
              builder: (c, snapshot) => ListTile(
                leading: (snapshot.data == BluetoothDeviceState.connected)
                    ? Icon(Icons.bluetooth_connected)
                    : Icon(Icons.bluetooth_disabled),
                title: Text(
                    'Device is ${snapshot.data.toString().split('.')[1]}.'),
                subtitle: Text('${device.id}'),
                trailing: StreamBuilder<bool>(
                  stream: device.isDiscoveringServices,
                  initialData: false,
                  builder: (c, snapshot) => IndexedStack(
                    index: snapshot.data! ? 1 : 0,
                    children: <Widget>[
                      IconButton(
                        icon: Icon(Icons.refresh),
                        onPressed: () => device.discoverServices(),
                      ),
                      IconButton(
                        icon: SizedBox(
                          child: CircularProgressIndicator(
                            valueColor: AlwaysStoppedAnimation(Colors.grey),
                          ),
                          width: 18.0,
                          height: 18.0,
                        ),
                        onPressed: null,
                      )
                    ],
                  ),
                ),
              ),
            ),
            StreamBuilder<int>(
              stream: device.mtu,
              initialData: 0,
              builder: (c, snapshot) => ListTile(
                title: Text('MTU Size'),
                subtitle: Text('${snapshot.data} bytes'),
                trailing: IconButton(
                  icon: Icon(Icons.edit),
                  onPressed: () => device.requestMtu(223),
                ),
              ),
            ),
            StreamBuilder<List<BluetoothService>>(
              stream: device.services,
              initialData: [],
              builder: (c, snapshot) {
                return Column(
                  children: _buildServiceTiles(snapshot.data!),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

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

1 回复

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


flutter_blue 是一个用于 Flutter 应用的蓝牙通信插件,支持 Android 和 iOS 平台。它提供了丰富的 API 来发现、连接、发送和接收数据到蓝牙设备。以下是如何使用 flutter_blue 插件的基本步骤。

1. 添加依赖

首先,在 pubspec.yaml 文件中添加 flutter_blue 依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_blue: ^0.8.0

然后运行 flutter pub get 来安装依赖。

2. 导入包

在你的 Dart 文件中导入 flutter_blue 包:

import 'package:flutter_blue/flutter_blue.dart';

3. 初始化 FlutterBlue

在应用启动时,初始化 FlutterBlue:

FlutterBlue flutterBlue = FlutterBlue.instance;

4. 扫描蓝牙设备

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

flutterBlue.startScan(timeout: Duration(seconds: 4));

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

5. 停止扫描

扫描完成后,调用 stopScan 方法停止扫描:

flutterBlue.stopScan();

6. 连接设备

选择你要连接的设备,并调用 connect 方法:

BluetoothDevice device = ...; // 从扫描结果中获取设备

device.connect().then((_) {
  print('Connected to device: ${device.name}');
});

7. 发现服务

连接成功后,你可以发现设备的服务:

device.discoverServices().then((services) {
  for (BluetoothService service in services) {
    print('Service: ${service.uuid}');
  }
});

8. 读取特征值

你可以通过服务的特征值来读取或写入数据:

BluetoothCharacteristic characteristic = ...; // 从服务中获取特征值

characteristic.read().then((value) {
  print('Read value: $value');
});

9. 写入特征值

你也可以向特征值写入数据:

List<int> data = [0x01, 0x02, 0x03];
characteristic.write(data).then((_) {
  print('Data written');
});

10. 断开连接

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

device.disconnect().then((_) {
  print('Disconnected from device');
});

权限配置

在 Android 和 iOS 上使用蓝牙功能时,需要配置相应的权限。

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 Bluetooth access to connect to devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>We need Bluetooth access to connect to devices</string>

注意事项

  • 蓝牙功能在某些设备上可能需要用户授权,特别是位置权限。
  • 不同蓝牙设备的服务和特征值可能不同,需要根据具体设备的BLE协议来进行操作。

示例代码

以下是一个简单的示例,展示了如何使用 flutter_blue 进行蓝牙通信:

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BluetoothApp(),
    );
  }
}

class BluetoothApp extends StatefulWidget {
  [@override](/user/override)
  _BluetoothAppState createState() => _BluetoothAppState();
}

class _BluetoothAppState extends State<BluetoothApp> {
  FlutterBlue flutterBlue = FlutterBlue.instance;
  BluetoothDevice? connectedDevice;
  List<BluetoothService> services = [];

  [@override](/user/override)
  void initState() {
    super.initState();
    _startScan();
  }

  void _startScan() {
    flutterBlue.startScan(timeout: Duration(seconds: 4));
    flutterBlue.scanResults.listen((results) {
      for (ScanResult result in results) {
        print('Found device: ${result.device.name} (${result.device.id})');
        if (result.device.name == 'YourDeviceName') {
          _connectToDevice(result.device);
          break;
        }
      }
    });
  }

  void _connectToDevice(BluetoothDevice device) async {
    await device.connect();
    setState(() {
      connectedDevice = device;
    });
    _discoverServices(device);
  }

  void _discoverServices(BluetoothDevice device) async {
    List<BluetoothService> services = await device.discoverServices();
    setState(() {
      this.services = services;
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Blue Example'),
      ),
      body: ListView(
        children: services.map((service) {
          return ListTile(
            title: Text('Service: ${service.uuid}'),
            subtitle: Text('Characteristics: ${service.characteristics.length}'),
          );
        }).toList(),
      ),
    );
  }
}
回到顶部