flutter windows 桌面应用程序: 读取ble蓝牙扫码枪数据

844 阅读8分钟

目标

  1. 能够监听电脑本身的蓝牙适配器状态
  2. 能够发现以及停止发现附近的ble蓝牙设备
  3. 连接指定的ble蓝牙设备
  4. 订阅指定ble蓝牙设备的特征数据
  5. 取消订阅指定ble蓝牙设备的特征数据
  6. 资源释放

环境说明

操作系统: 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));
        }
      });
    }
  }
}

效果

image.png

image.png

选择对应扫码枪的ble蓝牙设备名之后,弹窗会关闭(并会建立与该设备的连接),此时再点击主界面的订阅扫码枪按钮,则能够监听到ble蓝牙扫码枪返回的数据了

如下是正常模式下的结果(扫到一个条码,就会立即回传)

image.png

如下是盘点模式下的结果 扫描说明书上,盘点模式的条形码,扫码枪会切换为扫码模式,再扫描上传统计数据的条形码,会将当前条码枪在盘点模式中已录入的总条码数返回

image.png

扫描说明书上上传所有数据的条形码,会将所有条码一条一条不停的回传,直至所有条码都传送完毕(不足: 程序无法直观的判断是否全部上传完毕)

image.png

特别说明

中文显示问题,请看我之前的文章:win11 桌面开发 flutter3.x 中文字体和对齐问题 - 掘金 (juejin.cn)

ble调试助手很好用:下载链接 BLEAssist.ZIP

总结

  1. 基本能满足需求
  2. 不足的地方在于,程序不能控制扫码枪峰鸣,不能主动获取扫码枪的电量,扫码抢所有返回的数据都是通过一个特征返回的,无法直观的区分(当然,这些不足的地方理论上可以由扫码枪厂家,提供这些能力,但估计得加钱,遂忽略)