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
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>