目标
- 能够监听电脑本身的蓝牙适配器状态
- 能够发现以及停止发现附近的ble蓝牙设备
- 连接指定的ble蓝牙设备
- 订阅指定ble蓝牙设备的特征数据
- 取消订阅指定ble蓝牙设备的特征数据
- 资源释放
环境说明
操作系统: Windows 11
flutter版本:
Flutter 3.13.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision d211f42860 (2 weeks ago) • 2023-10-25 13:42:25 -0700
Engine • revision 0545f8705d
Tools • Dart 3.1.5 • DevTools 2.25.0
依赖插件
flutter pub add win_ble
定义util类, 封装 win_ble 组件相关方法
win_ble_util.dart
import 'dart:async';
import 'dart:typed_data';
import 'package:win_ble/win_ble.dart';
import 'package:win_ble/win_file.dart';
typedef OnScanedBleDevice = Function(BleDevice);
typedef OnBleStateChanged = Function(BleState);
typedef OnCharacteristicValueStream = Function(SubscribeToCharacteristicData?);
/// ble蓝牙设备连接回调方法
/// [connectResult] 连接结果. true表示连接成功, false表示连接失败
typedef OnConnectedCall = Function(bool connectResult);
/// 特征返回的数据
class SubscribeToCharacteristicData {
static const _mapKeydeviceAddr = 'address';
static const _mapKeyServiceId = 'serviceId';
static const _mapKeyCharacteristicId = 'characteristicId';
static const _mapKeyValue = 'value';
/// ble设备地址
String deviceAddr;
/// ble设备服务id
String serviceId;
/// ble设备特征id
String characteristicId;
/// 返回的数据
List<dynamic>? data;
SubscribeToCharacteristicData(
{required this.deviceAddr,
required this.serviceId,
required this.characteristicId,
this.data});
factory SubscribeToCharacteristicData.fromMap(Map map) {
return SubscribeToCharacteristicData(
deviceAddr: map[_mapKeydeviceAddr],
serviceId: map[_mapKeyServiceId],
characteristicId: map[_mapKeyCharacteristicId],
data: map[_mapKeyValue]);
}
/// 将data转换为List<int>数据类型
List<int>? dataToListInt() {
return data?.cast<int>();
}
/// 将data转换为String数据类型
/// [removeLineBreaks] 是否去除回车/换行符. 默认:false
String? dataToString([bool removeLineBreaks = false]) {
List<int>? list = dataToListInt();
if (list != null) {
String str = String.fromCharCodes(list);
if (removeLineBreaks) {
str = str.replaceAll("\r\n", "");
str = str.replaceAll("\n", "");
str = str.replaceAll("\r", "");
}
return str;
} else {
return null;
}
}
}
/// WinBle组件工具
/// 大概使用流程
/// 1. listenBleState方法,监听蓝牙适配器的状态
/// 2. init方法,对WinBle组件进行初始化
/// 3. scan方法,发现附近的ble设备
/// 4. connect方法,连接指定的ble设备
/// 5. discoverServices方法,获取已连接ble设备提供的所有服务
/// 6. discoverCharacteristics方法,获取已连接ble设备的指定服务下的所有特征
/// 7. readData方法,从已连接ble设备的指定服务的指定特征下,读取一次数据
/// 8. writeDate方法,向已连接ble设备的指定服务的指定特征下,写入一次数据
/// 9. subscribeToCharacteristic方法,订阅已连接ble设备的指定服务的指定特征的数据流,可持续获取到该特征向当前ble适配器发送的数据
/// 10. unSubscribeFromCharacteristic方法,取消对已连接ble设备的指定服务的指定特征的数据流的订阅
/// 11. disconnect方法,断开与已连接ble设备的连接
/// 12. stopListenBleState方法,停止对ble适配器状态的监听
/// 13. stopScan方法,停止发现附近的ble设备
/// 14. dispose方法,释放所有资源
///
/// P.S. openBleAdaptor方法可以打开蓝牙适配器,closeBleAdaptor方法可以关闭蓝牙适配器,getMaxMtuSize方法可以获取已连接ble设备的MaxMtuSize
///
/// 以实时获取蓝牙BLE扫描枪扫描的数据为例: (假设蓝牙扫描枪的名称为 Gun_BLE, 假设读取扫描枪数据的服务id和特征id为: xxx, yyy)
///
/// 以下是伪代码
/// ```dart
/// BleDevice? _connectedBleDevice;
/// StreamSubscription<dynamic>? _characteristicStreamSubscription;
///
/// // 初始化WinBle组件
/// WinBleUtil.instance.init();
/// // 发现附近的ble设备
/// WinBleUtil.instance.scan((BleDevice bleDevice){
/// if(bleDevice.name=='Gun_BLE'){
/// // 找到了特定设备
/// _connectedBleDevice = bleDevice;
/// // 停止ble设备发现
/// WinBleUtil.instance.stopScan();
/// // 建立与ble设备的连接
/// WinBleUtil.instance.connect(_connectedBleDevice,(bool connectResult){
/// if(connectResult){
/// // 与ble设备连接成功,订阅指定特征的数据流,用于实时感知ble扫码枪发过来的数据
/// _characteristicStreamSubscription = WinBleUtil.instance.subscribeToCharacteristic('xxx','yyy',(dynamic data){
/// if(data!=null){
/// // 处理接收到的数据
/// debugPrint(data.runtimeType)
/// }
/// });
/// }
/// });
/// }
/// });
///
/// // 最后在合适的时机释放资源
/// if(null!=_characteristicStreamSubscription){
/// _characteristicStreamSubscription.cancel();
/// _characteristicStreamSubscription=null;
/// }
/// WinBleUtil.instance.dispose();
/// ```
class WinBleUtil {
static WinBleUtil? _instance;
WinBleUtil._();
static WinBleUtil get instance => _instance ??= WinBleUtil._();
StreamSubscription<BleDevice>? _scanBleDeviceStreamSubscription;
StreamSubscription<BleState>? _bleStateStreamSubscription;
StreamSubscription<bool>? _connectionStreamSubscription;
/// 当前连接上的ble蓝牙设备
BleDevice? _connectedBleDevice;
String get connectedBleDeviceName =>
null != _connectedBleDevice ? _connectedBleDevice!.name : '';
/// 当前是否完成了WinBle组件的初始化
bool _isInited = false;
/// 当前是否正在发现附近的ble蓝牙设备
bool _isScaning = false;
/// ble蓝牙组件初始化
Future<void> init() async {
if (_isInited == false) {
_isInited = true;
WinBle.initialize(serverPath: await WinServer.path, enableLog: false);
}
}
/// 监听蓝牙适配器状态
void listenBleState(OnBleStateChanged onBleStateChanged) {
_bleStateStreamSubscription = WinBle.bleState.listen(onBleStateChanged);
}
/// 打开蓝牙适配器
void openBleAdaptor(OnScanedBleDevice? onScanedBleDevice) {
WinBle.updateBluetoothState(true);
scan(onScanedBleDevice);
}
/// 关闭蓝牙适配器
void closeBleAdaptor() {
disconnect();
stopScan();
WinBle.updateBluetoothState(false);
}
/// 发现附近的ble蓝牙设备
void scan(OnScanedBleDevice? onScanedBleDevice) {
if (!_isScaning) {
_isScaning = true;
if (null != onScanedBleDevice) {
_scanBleDeviceStreamSubscription =
WinBle.scanStream.listen(onScanedBleDevice);
}
WinBle.startScanning();
}
}
bool hasConnectedBleDevice() {
return _connectedBleDevice != null;
}
/// 连接指定的ble蓝牙设备
/// [bleDevice] ble蓝牙设备
/// [onConnectedCall] 连接回调方法, 该方法会返回连接结果
Future<void> connect(
BleDevice bleDevice, OnConnectedCall onConnectedCall) async {
_connectedBleDevice = bleDevice;
_connectionStreamSubscription =
WinBle.connectionStreamOf(bleDevice.address).listen((onConnectedCall));
await WinBle.connect(bleDevice.address);
}
/// 获取已连接ble设备的maxMtuSize, 如果没有已连接设备,则返回 Future<null>
Future<dynamic> getMaxMtuSize() {
if (_connectedBleDevice != null) {
return WinBle.getMaxMtuSize(_connectedBleDevice!.address);
}
return Future.value(null);
}
/// 获取已连接ble设备的服务列表, 如果没有已连接设备,则返回 Future<null>
Future<List<String>?> discoverServices() {
if (_connectedBleDevice != null) {
return WinBle.discoverServices(_connectedBleDevice!.address);
}
return Future.value(null);
}
/// 获取已连接ble设备的指定服务的BleCharacteristic列表, 如果没有已连接设备,则返回 Future<null>
/// [serviceId] 已连接ble设备的服务id
Future<List<BleCharacteristic>?> discoverCharacteristics(
String serviceId) async {
if (_connectedBleDevice != null) {
return WinBle.discoverCharacteristics(
address: _connectedBleDevice!.address, serviceId: serviceId);
}
return Future.value(null);
}
/// 从已连接设备的,指定服务的,指定BleCharacteristic中,读取一次数据,如果没有已连接设备,则返回 Future<null>
/// [serviceId] 服务标识
/// [characteristicId] 特征标识
Future<List<int>?> readData(String serviceId, String characteristicId) async {
if (_connectedBleDevice != null) {
List<int> data = await WinBle.read(
address: _connectedBleDevice!.address,
serviceId: serviceId,
characteristicId: characteristicId);
return data;
} else {
return Future.value(null);
}
}
/// 向已连接设备的,指定服务的,指定BleCharacteristic中,写入数据
/// [serviceId] 服务标识
/// [characteristicId] 特征标识
/// [data] 待写入的数据
Future<void> writeData(
String serviceId, String characteristicId, Uint8List data) async {
if (_connectedBleDevice != null) {
await WinBle.write(
address: _connectedBleDevice!.address,
service: serviceId,
characteristic: characteristicId,
data: data,
writeWithResponse: false);
}
}
/// 订阅已连接设备的,指定服务的,指定BleCharacteristic的connectionStream,当不存在已连接设备时,则返回null
/// [serviceId] 服务标识
/// [characteristicId] 特征标识
/// [onCharacteristicValueStream] BleCharacteristic有响应数据时,执行的回调
StreamSubscription<dynamic>? subscribeToCharacteristic(
String serviceId,
String characteristicId,
OnCharacteristicValueStream onCharacteristicValueStream) {
if (_connectedBleDevice != null) {
WinBle.subscribeToCharacteristic(
address: _connectedBleDevice!.address,
serviceId: serviceId,
characteristicId: characteristicId);
return WinBle.characteristicValueStream.listen((data) {
SubscribeToCharacteristicData? val = data != null &&
data.runtimeType.toString() == '_Map<dynamic, dynamic>'
? SubscribeToCharacteristicData.fromMap(data)
: null;
onCharacteristicValueStream(val);
});
}
return null;
}
/// 取消对已连接蓝牙设备的,指定服务的,指定BleCharacteristic的connectionStream订阅
/// [serviceId] 服务标识
/// [characteristicId] 特征标识
Future<void> unSubscribeFromCharacteristic(
String serviceId, String characteristicId) async {
if (_connectedBleDevice != null) {
await WinBle.unSubscribeFromCharacteristic(
address: _connectedBleDevice!.address,
serviceId: serviceId,
characteristicId: characteristicId);
}
}
/// 断开与已连接的ble设备的连接
void disconnect() {
if (null != _connectedBleDevice) {
WinBle.disconnect(_connectedBleDevice?.address);
_connectionStreamSubscription?.cancel();
_connectedBleDevice = null;
}
}
/// 取消对ble蓝牙适配器的状态改变监听
void stopListenBleState() {
_bleStateStreamSubscription?.cancel();
}
/// 停止发现附近的ble设备
void stopScan() {
_scanBleDeviceStreamSubscription?.cancel();
WinBle.stopScanning();
_isScaning = false;
}
/// 释放所有资源(断开连接,停止对附近ble设备的发现,停止监听ble蓝牙适配器状态,关闭ble蓝牙适配器,执行WinBle.dispose)
dispose() {
stopListenBleState();
disconnect();
closeBleAdaptor();
WinBle.dispose();
}
}
barcode_scanner_ble_util.dart
import 'dart:async';
import 'package:win_ble_demo/src/util/win_ble_util.dart';
class BarcodeScannerBleUtil {
static BarcodeScannerBleUtil? _instance;
// Avoid self instance
BarcodeScannerBleUtil._();
static BarcodeScannerBleUtil get instance =>
_instance ??= BarcodeScannerBleUtil._();
static String _deviceName = 'BarCode Bluetooth BLE';
static const String _serviceId = '0000feea-0000-1000-8000-00805f9b34fb';
static const String _characteristicsId =
'00002aa1-0000-1000-8000-00805f9b34fb';
/// 订阅该特征,就可以收到扫码枪返回的数据
StreamSubscription<dynamic>? subscribeToCharacteristic(
OnCharacteristicValueStream onCharacteristicValueStream) {
return WinBleUtil.instance.subscribeToCharacteristic(
_serviceId, _characteristicsId, onCharacteristicValueStream);
}
/// 取消特征订阅
Future<void> unSubscribeFromCharacteristic() {
return WinBleUtil.instance
.unSubscribeFromCharacteristic(_serviceId, _characteristicsId);
}
}
stateful_builder.dart
import 'package:flutter/material.dart';
class StatefulBuilder extends StatefulWidget {
final StatefulWidgetBuilder builder;
const StatefulBuilder({Key? key, required this.builder}) : super(key: key);
@override
_StatefulBuilderState createState() => _StatefulBuilderState();
}
class _StatefulBuilderState extends State<StatefulBuilder> {
@override
Widget build(BuildContext context) => widget.builder(context, setState);
}
定义ble蓝牙适配器状态组件
实现ble蓝牙适配器状态改变监听,以及连接指定的ble蓝牙设备
ble_status_widget.dart
import 'package:flutter/material.dart';
import 'package:win_ble/win_ble.dart';
import 'package:win_ble_demo/src/util/barcode_scanner_ble_util.dart';
import 'package:win_ble_demo/src/util/win_ble_util.dart';
class BleStatusWidget extends StatefulWidget {
const BleStatusWidget({Key? key}) : super(key: key);
@override
_BleStatusWidgetState createState() => _BleStatusWidgetState();
}
typedef stateStateMethod = void Function(void Function());
class _BleStatusWidgetState extends State<BleStatusWidget> {
String _statusStr = '未知';
final List<BleDevice> _bleDeviceList = [];
stateStateMethod? _setStateMethod;
@override
void initState() {
super.initState();
WinBleUtil.instance.init();
WinBleUtil.instance.listenBleState((p0) {
switch (p0) {
case BleState.Disabled:
_statusStr = '禁用';
break;
case BleState.On:
_statusStr = '开启';
break;
case BleState.Off:
_statusStr = '关闭';
break;
case BleState.Unsupported:
_statusStr = '不支持';
break;
default:
_statusStr = '未知';
}
debugPrint(
'>>>>>>>>>>>>>>>>>>>>>>>>>>>状态变更<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
setState(() {});
});
}
@override
void dispose() {
debugPrint(
'>>>>>>>>>>>>>>>>>>>>>>>>>>>dispose被调用<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
super.dispose();
BarcodeScannerBleUtil.instance.unSubscribeFromCharacteristic();
WinBleUtil.instance.dispose();
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => onOpenOrCloseBleAdaptor(context),
child: Text(_statusStr));
}
void onOpenOrCloseBleAdaptor(BuildContext context) {
WinBleUtil.instance.scan((p0) {
int idx =
_bleDeviceList.indexWhere((element) => element.address == p0.address);
if (p0.name.isNotEmpty && idx < 0) {
_bleDeviceList.add(p0);
if (_setStateMethod != null) {
_setStateMethod!(() {});
}
} else if (idx >= 0) {
String oldName = _bleDeviceList[idx].name;
p0.name = oldName;
_bleDeviceList[idx] = p0;
}
});
showListDialog();
}
Future<void> showListDialog() async {
return await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
var child = Column(
children: <Widget>[
const ListTile(title: Text("ble设备搜索中...")),
Expanded(
child:
StatefulBuilder(builder: (BuildContext context, setState_) {
_setStateMethod = setState_;
return ListView.builder(
itemCount: _bleDeviceList.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
"${_bleDeviceList[index].name}-${_bleDeviceList[index].address}-${_bleDeviceList[index].advType}"),
onTap: () =>
onConnectedDevice(_bleDeviceList[index], context),
);
},
);
}),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () => onCloseDialog(context),
child: const Text('关闭'))
],
)
],
);
return Dialog(child: child);
},
);
}
void onCloseDialog(BuildContext context) {
_bleDeviceList.clear();
WinBleUtil.instance.stopScan();
Navigator.of(context).pop();
}
void onConnectedDevice(BleDevice device, BuildContext context) {
WinBleUtil.instance.connect(device, (connectResult) {
if (connectResult) {
WinBleUtil.instance.stopScan();
_bleDeviceList.clear();
_statusStr = '已连接设备:${WinBleUtil.instance.connectedBleDeviceName}';
setState(() {});
Navigator.of(context).pop();
}
});
}
}
最后是main.dart
main.dart
import 'package:flutter/material.dart';
import 'package:win_ble_demo/src/util/barcode_scanner_ble_util.dart';
import 'package:win_ble_demo/src/util/win_ble_util.dart';
import 'package:win_ble_demo/src/widget/ble_status_widget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
actions: const [
Row(
children: [BleStatusWidget()],
)
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
TextButton(
onPressed: onSubscribeToCharacteristic,
child: const Text('订阅扫描枪')),
],
),
),
);
}
void onSubscribeToCharacteristic() {
if (WinBleUtil.instance.hasConnectedBleDevice()) {
BarcodeScannerBleUtil.instance.subscribeToCharacteristic((p0) {
if (null != p0) {
debugPrint(p0.dataToString(true));
}
});
}
}
}
效果
选择对应扫码枪的ble蓝牙设备名之后,弹窗会关闭(并会建立与该设备的连接),此时再点击主界面的订阅扫码枪按钮,则能够监听到ble蓝牙扫码枪返回的数据了
如下是正常模式下的结果(扫到一个条码,就会立即回传)
如下是盘点模式下的结果
扫描说明书上,盘点模式的条形码,扫码枪会切换为扫码模式,再扫描上传统计数据的条形码,会将当前条码枪在盘点模式中已录入的总条码数返回
扫描说明书上上传所有数据的条形码,会将所有条码一条一条不停的回传,直至所有条码都传送完毕(不足: 程序无法直观的判断是否全部上传完毕)
特别说明
中文显示问题,请看我之前的文章:win11 桌面开发 flutter3.x 中文字体和对齐问题 - 掘金 (juejin.cn)
ble调试助手很好用:下载链接 BLEAssist.ZIP
总结
- 基本能满足需求
- 不足的地方在于,程序不能控制扫码枪峰鸣,不能主动获取扫码枪的电量,扫码抢所有返回的数据都是通过一个特征返回的,无法直观的区分(当然,这些不足的地方理论上可以由扫码枪厂家,提供这些能力,但估计得加钱,遂忽略)