Flutter蓝牙通信插件bluetooth_plus的使用

Flutter蓝牙通信插件bluetooth_plus的使用

引言

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

Alpha版本

该库正在与生产应用一起积极开发,API将在我们达到1.0版本之前不断演变。

  • 请充分准备处理破坏性更改。
  • 此插件必须在真实设备上进行测试。

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

跨平台蓝牙低功耗(BLE)

FlutterBlue旨在为iOS和Android提供最佳功能。

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

使用方法

获取一个实例

FlutterBlue flutterBlue = FlutterBlue.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字节)。

入门指南

更改Android的minSdkVersion

Flutter_blue仅从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"/>
iOS

ios/Runner/Info.plist中添加以下内容:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要BLE权限</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要BLE权限</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>需要位置权限</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限</string>

关于iOS的位置权限更多信息,请参阅:苹果官方文档

示例代码

// 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_bluetooth_plus/flutter_bluetooth_plus.dart';
import 'package:flutter_bluetooth_plus_example/widgets.dart';

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

class FlutterBlueApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      color: Colors.lightBlue,
      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 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'}.',
              style: Theme.of(context)
                  .primaryTextTheme
                  .titleMedium
                  ?.copyWith(color: Colors.white),
            ),
          ],
        ),
      ),
    );
  }
}

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,
                    style: Theme.of(context)
                        .primaryTextTheme
                        .labelLarge
                        ?.copyWith(color: Colors.white),
                  ));
            },
          )
        ],
      ),
      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蓝牙通信插件bluetooth_plus的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

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


bluetooth_plus 是一个用于在 Flutter 应用中实现蓝牙通信的插件。它提供了与蓝牙设备连接、扫描、读写数据等功能。以下是使用 bluetooth_plus 插件的基本步骤和示例代码。

1. 添加依赖

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

dependencies:
  flutter:
    sdk: flutter
  bluetooth_plus: ^1.1.0  # 请根据最新版本进行更新

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

2. 导入插件

在你的 Dart 文件中导入 bluetooth_plus 插件:

import 'package:bluetooth_plus/bluetooth_plus.dart';

3. 初始化蓝牙

在使用蓝牙功能之前,需要初始化蓝牙适配器:

BluetoothPlus bluetooth = BluetoothPlus.instance;

4. 检查蓝牙状态

在开始扫描或连接设备之前,检查蓝牙是否已启用:

bool isEnabled = await bluetooth.isEnabled();
if (!isEnabled) {
  await bluetooth.requestEnable();
}

5. 扫描蓝牙设备

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

bluetooth.startScan().listen((BluetoothDevice device) {
  print('Found device: ${device.name}, ${device.address}');
});

6. 停止扫描

当你不再需要扫描设备时,可以停止扫描:

bluetooth.stopScan();

7. 连接蓝牙设备

使用 connect 方法来连接指定的蓝牙设备:

BluetoothDevice device = ...; // 从扫描结果中获取设备
await bluetooth.connect(device.address);

8. 读取和写入数据

连接成功后,你可以通过 readwrite 方法与设备进行数据交互:

// 读取数据
List<int> data = await bluetooth.read();

// 写入数据
await bluetooth.write([0x01, 0x02, 0x03]);

9. 断开连接

当你不再需要与设备通信时,可以断开连接:

await bluetooth.disconnect();

10. 处理权限

在 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>我们需要访问蓝牙来连接设备</string>

完整示例

以下是一个完整的示例,展示了如何使用 bluetooth_plus 插件进行蓝牙设备的扫描、连接和数据传输:

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

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

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

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

class _BluetoothScreenState extends State<BluetoothScreen> {
  BluetoothPlus bluetooth = BluetoothPlus.instance;
  List<BluetoothDevice> devices = [];
  bool isConnected = false;

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

  Future<void> _checkBluetooth() async {
    bool isEnabled = await bluetooth.isEnabled();
    if (!isEnabled) {
      await bluetooth.requestEnable();
    }
  }

  Future<void> _scanDevices() async {
    devices.clear();
    bluetooth.startScan().listen((device) {
      setState(() {
        devices.add(device);
      });
    });
  }

  Future<void> _connectDevice(BluetoothDevice device) async {
    await bluetooth.connect(device.address);
    setState(() {
      isConnected = true;
    });
  }

  Future<void> _sendData() async {
    await bluetooth.write([0x01, 0x02, 0x03]);
  }

  Future<void> _disconnect() async {
    await bluetooth.disconnect();
    setState(() {
      isConnected = false;
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bluetooth Plus Example'),
      ),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: _scanDevices,
            child: Text('Scan Devices'),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: devices.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(devices[index].name),
                  subtitle: Text(devices[index].address),
                  onTap: () => _connectDevice(devices[index]),
                );
              },
            ),
          ),
          if (isConnected) ...[
            ElevatedButton(
              onPressed: _sendData,
              child: Text('Send Data'),
            ),
            ElevatedButton(
              onPressed: _disconnect,
              child: Text('Disconnect'),
            ),
          ],
        ],
      ),
    );
  }
}
回到顶部