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(),
      ),
    );
  }
}
回到顶部