Flutter中BLE蓝牙通讯的实现

打印 上一主题 下一主题

主题 1880|帖子 1880|积分 5640

配景:
之前用java写过安卓端的BLE蓝牙通讯测试的demo,最近学习了Flutter干系知识,准备以BLE蓝牙测试为例,写一个flutte的BLE蓝牙测试demo。之前的demo请参考:安卓BLE蓝牙通讯
实现:

  • 权限
① 在android下的AndroidManifest.xml文件中添加安卓设备所需权限。
  1.     <uses-permission android:name="android.permission.BLUETOOTH" />
  2.     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  3.     <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
  4.     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  5.     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  6.     <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
  7.     <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
复制代码
② 在ios下的Info.plist文件中添加ios设备所需权限。
  1.         <key>NSBluetoothAlwaysUsageDescription</key>
  2.     <string>需要使用蓝牙来连接设备</string>
  3.     <key>NSBluetoothPeripheralUsageDescription</key>
  4.     <string>需要使用蓝牙来连接设备</string>
  5.     <key>NSLocationWhenInUseUsageDescription</key>
  6.     <string>需要使用位置权限来搜索附近的蓝牙设备</string>
  7.     <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
  8.     <string>需要使用位置权限来搜索附近的蓝牙设备</string>
复制代码

  • 动态权限申请
① 创建一个PermissionUtil来管理权限获取。
  1. import 'package:permission_handler/permission_handler.dart';
  2. class PermissionUtil {
  3.   /// 请求蓝牙和位置信息权限
  4.   static Future<bool> requestBluetoothConnectPermission() async {
  5.     Map<Permission, PermissionStatus> permission = await [
  6.       Permission.bluetoothConnect,
  7.       Permission.bluetoothScan,
  8.       Permission.bluetoothAdvertise,
  9.       Permission.location,
  10.     ].request();
  11.     if (await Permission.bluetoothConnect.isGranted) {
  12.       print("蓝牙连接权限申请通过");
  13.     } else {
  14.       print("蓝牙连接权限申请失败");
  15.       return false;
  16.     }
  17.     if (await Permission.bluetoothScan.isGranted) {
  18.       print("蓝牙扫描权限申请通过");
  19.     } else {
  20.       print("蓝牙扫描权限申请失败");
  21.       return false;
  22.     }
  23.     if (await Permission.bluetoothAdvertise.isGranted) {
  24.       print("蓝牙广播权限申请通过");
  25.     } else {
  26.       print("蓝牙广播权限申请失败");
  27.       return false;
  28.     }
  29.     if (await Permission.location.isGranted) {
  30.       print("位置权限申请通过");
  31.     } else {
  32.       print("位置权限申请失败");
  33.       return false;
  34.     }
  35.     return true;
  36.   }
  37. }
复制代码
② 在主页面中调用动态权限获取
  1. PermissionUtil.requestBluetoothConnectPermission();
复制代码
③ 在蓝牙扫描时调用权限检测。
  1.   // 扫描蓝牙
  2.   void scanDevices() {
  3.     PermissionUtil.requestBluetoothConnectPermission().then((hasPermission) {
  4.       if(hasPermission) {
  5.         // 权限获取成功
  6.         devices.clear();
  7.         BleManager.getInstance().setCallback(this);
  8.         BleManager.getInstance().startScan(
  9.           timeout: Duration(seconds: 10),
  10.         );
  11.       } else {
  12.         // 权限获取失败
  13.         SnackBarManager.instance.showSnackBar("权限获取失败", "请先授予权限");
  14.       }
  15.     });
  16.   }
复制代码
④ ios设备无需进举措态权限获取。
3. 创建一个蓝牙管理类
① 创建一个蓝牙管理类来管理毗连的扫描、毗连、通讯等。
  1. import 'package:bluetooth/util/constants/ble_config.dart';
  2. import 'package:bluetooth/util/snack_bar_manager.dart';
  3. import 'package:flutter_blue_plus/flutter_blue_plus.dart';
  4. import '../inter/ble_callback.dart';
  5. class BleManager {
  6.   static BleManager? _instance;
  7.   BluetoothDevice? _device;
  8.   BluetoothCharacteristic? _writeCharacteristic;
  9.   BluetoothCharacteristic? _notifyCharacteristic;
  10.   // 回调接口
  11.   BleCallback? _callback;
  12.   // 设置回调
  13.   void setCallback(BleCallback callback) {
  14.     _callback = callback;
  15.   }
  16.   // 私有构造函数
  17.   BleManager._();
  18.   // 单例模式
  19.   static BleManager getInstance() {
  20.     _instance ??= BleManager._();
  21.     return _instance!;
  22.   }
  23.   // 检查蓝牙是否可用
  24.   Future<bool> isAvailable() async {
  25.     return await FlutterBluePlus.isAvailable;
  26.   }
  27.   // 检查蓝牙是否开启
  28.   Future<bool> isOn() async {
  29.     return await FlutterBluePlus.isOn;
  30.   }
  31.   // 开始扫描
  32.   Future<void> startScan({
  33.     Duration? timeout,
  34.     List<Guid>? withServices,
  35.   }) async {
  36.     if (!(await isOn())) {
  37.       SnackBarManager.instance.showSnackBar("蓝牙未开启", "请打开蓝牙");
  38.     }
  39.     // 停止之前的扫描
  40.     await stopScan();
  41.     // 监听扫描结果
  42.     FlutterBluePlus.scanResults.listen((results) {
  43.       for (ScanResult result in results) {
  44.         // print("扫描结果: ${result.device.name} - ${result.device.remoteId}");
  45.         if (_callback != null) {
  46.           _callback!.onScanResult(result.device);
  47.         }
  48.       }
  49.     });
  50.     // 开始扫描
  51.     print("开始扫描");
  52.     await FlutterBluePlus.startScan(
  53.       timeout: timeout ?? const Duration(seconds: 4),
  54.       withServices: withServices ?? [],
  55.     );
  56.   }
  57.   // 停止扫描
  58.   Future<void> stopScan() async {
  59.     await FlutterBluePlus.stopScan();
  60.   }
  61.   BluetoothDevice? getDeviceFromAddress(String address) {
  62.     try {
  63.       BluetoothDevice device = BluetoothDevice.fromId(address);
  64.       return device;
  65.     } catch (e) {
  66.       print('获取设备失败: $e');
  67.       return null;
  68.     }
  69.   }
  70.   // 连接设备
  71.   Future<bool> connect(BluetoothDevice device) async {
  72.     try {
  73.       await device.connect(
  74.         timeout: const Duration(seconds: 4),
  75.         autoConnect: false,
  76.       );
  77.       _device = device;
  78.       // 添加断开连接监听
  79.       device.connectionState.listen((BluetoothConnectionState state) {
  80.         if (state == BluetoothConnectionState.disconnected) {
  81.           // 设备断开连接
  82.           if (_callback != null) {
  83.             _callback!.onDisconnected();
  84.           }
  85.           _device = null;
  86.           _writeCharacteristic = null;
  87.           _notifyCharacteristic = null;
  88.         }
  89.       });
  90.       // 发现服务
  91.       List<BluetoothService> services = await device.discoverServices();
  92.       for (BluetoothService service in services) {
  93.         if (service.uuid.toString() == BleConfig.SERVICE_UUID) {
  94.           for (BluetoothCharacteristic characteristic in service.characteristics) {
  95.             if (characteristic.uuid.toString() == BleConfig.WRITE_CHARACTERISTIC_UUID) {
  96.               _writeCharacteristic = characteristic;
  97.             }
  98.             if (characteristic.uuid.toString() == BleConfig.NOTIFY_CHARACTERISTIC_UUID) {
  99.               _notifyCharacteristic = characteristic;
  100.             }
  101.           }
  102.         }
  103.       }
  104.       if (_notifyCharacteristic != null) {
  105.         // 设置通知
  106.         await enableNotification();
  107.         if (_callback != null) {
  108.           _callback!.onConnectSuccess();
  109.         }
  110.         return true;
  111.       } else {
  112.         print("未找到指定特征值");
  113.         return false;
  114.       }
  115.     } catch (e) {
  116.       if (_callback != null) {
  117.         _callback!.onConnectFailed(e.toString());
  118.       }
  119.       disconnect();
  120.       return false;
  121.     }
  122.   }
  123.   // 断开连接
  124.   Future<void> disconnect() async {
  125.     if (_device != null) {
  126.       await _device!.disconnect();
  127.       _device = null;
  128.       _notifyCharacteristic = null;
  129.       _writeCharacteristic = null;
  130.     }
  131.   }
  132.   // 发送数据
  133.   Future<bool> sendData(List<int> data) async {
  134.     if (_writeCharacteristic != null) {
  135.       await _writeCharacteristic!.write(data);
  136.       return true;
  137.     } else {
  138.       print("未持有WRITE_UUID");
  139.       return false;
  140.     }
  141.   }
  142.   // 启用通知
  143.   Future<void> enableNotification() async {
  144.     if (_notifyCharacteristic != null) {
  145.       await _notifyCharacteristic!.setNotifyValue(true);
  146.       _notifyCharacteristic!.value.listen((value) {
  147.         if (_callback != null) {
  148.           _callback!.onDataReceived(value);
  149.         }
  150.       });
  151.     } else {
  152.       print("未持有NOTIFY_UUID");
  153.     }
  154.   }
  155.   // 获取连接状态
  156.   bool isConnected() {
  157.     return _device != null && _notifyCharacteristic != null;
  158.   }
  159. }
复制代码
② 创建BleCallback来处置惩罚毗连回调。
  1. import 'package:flutter_blue_plus/flutter_blue_plus.dart';
  2. mixin BleCallback {
  3.   // 扫描结果回调
  4.   void onScanResult(BluetoothDevice device);
  5.   // 连接成功回调
  6.   void onConnectSuccess();
  7.   // 断开连接回调
  8.   void onDisconnected();
  9.   // 连接失败回调
  10.   void onConnectFailed(String error);
  11.   // 数据接收回调
  12.   void onDataReceived(List<int> data);
  13. }
复制代码
③ 统一管理蓝牙的UUID。
  1. class BleConfig {
  2.   // 服务和特征值 UUID
  3.   static const String SERVICE_UUID = "0783b03e-8535-b5a0-7140-a304d2495cb7";
  4.   static const String WRITE_CHARACTERISTIC_UUID = "0783b03e-8535-b5a0-7140-a304d2495cba";
  5.   static const String NOTIFY_CHARACTERISTIC_UUID = "0783b03e-8535-b5a0-7140-a304d2495cb8";
  6. }
复制代码

  • 创建页面来展示蓝牙通讯
① 创建一个页面来展示蓝牙的毗连和通讯状况。
  1. import 'package:flutter/material.dart';
  2. import 'package:get/get.dart';
  3. import '../home/home_contolller.dart';
  4. class BluetoothInfoPage extends StatelessWidget {
  5.   const BluetoothInfoPage({super.key});
  6.   @override
  7.   Widget build(BuildContext context) {
  8.     return GetBuilder<HomeController>(builder: (controller) {
  9.       return Scaffold(
  10.         appBar: AppBar(
  11.           title: Text('蓝牙信息'),
  12.         ),
  13.         body: Padding(
  14.           padding: const EdgeInsets.all(16.0),
  15.           child: Column(
  16.             crossAxisAlignment: CrossAxisAlignment.start,
  17.             children: [
  18.               Obx(() =>
  19.                   Text("连接状态: ${controller.bluetoothInfo.value
  20.                       .isConnected ? "已连接" : "未连接"}")),
  21.               SizedBox(height: 8),
  22.               Obx(() =>
  23.                   Text("蓝牙名称: ${controller.bluetoothInfo.value
  24.                       .name}")),
  25.               SizedBox(height: 8),
  26.               Obx(() =>
  27.                   Text("蓝牙地址: ${controller.bluetoothInfo.value
  28.                       .address}")),
  29.               SizedBox(height: 16),
  30.               Row(
  31.                 children: [
  32.                   Expanded(
  33.                     child: TextField(
  34.                       onChanged: (value) {
  35.                         controller.sentMessage.value = value; // 更新发送的消息
  36.                       },
  37.                       decoration: InputDecoration(
  38.                         labelText: '发送的报文',
  39.                       ),
  40.                     ),
  41.                   ),
  42.                   IconButton(
  43.                     icon: Icon(Icons.send),
  44.                     onPressed: () {
  45.                       controller.sendMessage();
  46.                     },
  47.                     tooltip: '发送消息',
  48.                   ),
  49.                 ],
  50.               ),
  51.               SizedBox(height: 16),
  52.               const Text("接收的报文:", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
  53.               const SizedBox(height: 8),
  54.               // 接收报文内容区域和按钮
  55.               Row(
  56.                 crossAxisAlignment: CrossAxisAlignment.start,
  57.                 children: [
  58.                   // 接收报文内容
  59.                   Expanded(
  60.                     child: Container(
  61.                       height: 200, // 固定高度
  62.                       decoration: BoxDecoration(
  63.                         border: Border.all(color: Colors.grey),
  64.                         borderRadius: BorderRadius.circular(4),
  65.                       ),
  66.                       child: Obx(() => SingleChildScrollView(
  67.                         child: Padding(
  68.                           padding: const EdgeInsets.all(8.0),
  69.                           child: Text(controller.receivedMessage.value),
  70.                         ),
  71.                       )),
  72.                     ),
  73.                   ),
  74.                   // 右侧按钮
  75.                   SizedBox(
  76.                     height: 200,  // 与内容区域等高
  77.                     child: Center(  // 使用 Center 包裹按钮
  78.                       child: IconButton(
  79.                         icon: Icon(Icons.delete),
  80.                         onPressed: () {
  81.                           controller.clearMessage();
  82.                         },
  83.                         tooltip: '清空接收内容',
  84.                       ),
  85.                     ),
  86.                   ),
  87.                 ],
  88.               ),
  89.             ],
  90.           ),
  91.         ),
  92.       );
  93.     });
  94.   }
  95. }
复制代码
② 创建一个页面来实现蓝牙的扫描和毗连。
  1. import 'package:flutter/cupertino.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
  4. import 'package:get/get_state_manager/src/simple/get_state.dart';
  5. import '../home/home_contolller.dart';
  6. class BluetoothListPage extends StatelessWidget {
  7.   const BluetoothListPage({super.key});
  8.   @override
  9.   Widget build(BuildContext context) {
  10.     return GetBuilder<HomeController>(builder: (controller)
  11.     {
  12.       return Scaffold(
  13.         appBar: AppBar(
  14.           title: Text('蓝牙设备列表'),
  15.         ),
  16.         body: Obx(() {
  17.           return RefreshIndicator(
  18.             onRefresh: () async {
  19.               controller.scanDevices(); // 下拉刷新时重新扫描
  20.             },
  21.             child: ListView.builder(
  22.               itemCount: controller.devices.length,
  23.               itemBuilder: (context, index) {
  24.                 final device = controller.devices[index];
  25.                 return Card(
  26.                   margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
  27.                   elevation: 2,
  28.                   child: ListTile(
  29.                     contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  30.                     title: Text(
  31.                       device.name,
  32.                       style: TextStyle(
  33.                           fontWeight: FontWeight.bold,
  34.                           fontSize: 16
  35.                       ),
  36.                     ),
  37.                     subtitle: Column(
  38.                       crossAxisAlignment: CrossAxisAlignment.start,
  39.                       children: [
  40.                         SizedBox(height: 4),
  41.                         Text(
  42.                             device.address,
  43.                             style: TextStyle(fontSize: 14)
  44.                         ),
  45.                       ],
  46.                     ),
  47.                     trailing: Container(
  48.                       padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
  49.                       decoration: BoxDecoration(
  50.                           color: device.isConnected ? Colors.green[100] : Colors.grey[200],
  51.                           borderRadius: BorderRadius.circular(12)
  52.                       ),
  53.                       child: Text(
  54.                         device.isConnected ? "已连接" : "未连接",
  55.                         style: TextStyle(
  56.                             color: device.isConnected ? Colors.green[700] : Colors.grey[700],
  57.                             fontSize: 14
  58.                         ),
  59.                       ),
  60.                     ),
  61.                     onTap: () {
  62.                       controller.connectToDevice(context, device);
  63.                     },
  64.                   ),
  65.                 );
  66.               },
  67.             )
  68.           );
  69.         }),
  70.       );
  71.     });
  72.   }
  73. }
复制代码

  • 创建一个Controller来管理页面的数据。
① 创建一个HomeController来管理页面数据以及蓝牙的现实毗连、通讯等。
  1. import 'package:bluetooth/util/ble_manager.dart';import 'package:bluetooth/util/permission_util.dart';import 'package:bluetooth/util/ua200_receiver.dart';import 'package:flutter/material.dart';import 'package:flutter_blue_plus/flutter_blue_plus.dart';import 'package:get/get.dart';import '../../data/bluetooth_device_info.dart';import '../../inter/ble_callback.dart';import '../../util/snack_bar_manager.dart';import '../../widget/loading_dialog.dart';import '../ble/bluetooth_info_page.dart';import '../ble/bluetooth_list_page.dart';class HomeController extends GetxController with BleCallback {  final List<Widget> pageList = [    const BluetoothInfoPage(),    const BluetoothListPage(),  ];  /// 当前界面的索引值  int currentIndex = 0;  var bluetoothInfo = BluetoothDeviceInfo(    name: "",    address: "",    isConnected: false,  ).obs;  var sentMessage = "".obs;  var receivedMessage = "".obs;  var devices = <BluetoothDeviceInfo>[].obs;  changeIndex(int index) {    currentIndex = index;    update();    if (devices.isEmpty && currentIndex == 1) {      scanDevices();    }  }  @override  void onInit() {    super.onInit();    PermissionUtil.requestBluetoothConnectPermission();
  2.   }  @override  void onReady() {    super.onReady();  }  // 扫描蓝牙
  3.   void scanDevices() {
  4.     PermissionUtil.requestBluetoothConnectPermission().then((hasPermission) {
  5.       if(hasPermission) {
  6.         // 权限获取成功
  7.         devices.clear();
  8.         BleManager.getInstance().setCallback(this);
  9.         BleManager.getInstance().startScan(
  10.           timeout: Duration(seconds: 10),
  11.         );
  12.       } else {
  13.         // 权限获取失败
  14.         SnackBarManager.instance.showSnackBar("权限获取失败", "请先授予权限");
  15.       }
  16.     });
  17.   }
  18.   Future<void> connectToDevice(BuildContext context, BluetoothDeviceInfo device) async {    if (device.isConnected) {      // 假如设备已毗连,则断开毗连      device.isConnected = false; // 断开毗连      BleManager.getInstance().disconnect();      bluetoothInfo.value = device;      devices.refresh();      return;    }    for (var dev in devices) {      dev.isConnected = false; // 先将所有设备的毗连状态设为 false    }    // 显示毗连中的状态框    // showDialog(    //   context: context,    //   barrierDismissible: false,    //   builder: (context) {    //     return const LoadingDialog();    //   },    // );    LoadingDialog.show("毗连中...");    // 这里可以判断毗连是否成功    var dev = BleManager.getInstance().getDeviceFromAddress(device.address);    if (dev != null) {      BleManager.getInstance().connect(dev).then((success) {        if (success) {          // 毗连成功          LoadingDialog.hide(); // 关闭毗连中的状态框          device.isConnected = true; // 将选中的设备毗连状态设为 true          devices.remove(device); // 移除该设备          devices.insert(0, device); // 将设备插入到列表顶部          bluetoothInfo.value = device;        } else {          LoadingDialog.hide(); // 关闭毗连中的状态框          SnackBarManager.instance.showSnackBar("毗连失败", "无法毗连到 ${device.name},请重试。");        }      });    } else {      LoadingDialog.hide(); // 关闭毗连中的状态框      SnackBarManager.instance.showSnackBar("毗连非常", "");    }  }  void sendMessage() {    // 发送消息的逻辑    print("发送消息: ${sentMessage.value}");    var data = sentMessage.value.codeUnits;    BleManager.getInstance().sendData(data).then((result) {      if (result) {        print("消息发送成功");      } else {        print("消息发送失败");        SnackBarManager.instance.showSnackBar("消息发送失败", "请检查蓝牙毗连状态");      }    });  }  void clearMessage() {    receivedMessage.value = "";  }  @override  void onClose() {    super.onClose();  }  @override  void onScanResult(BluetoothDevice device) {    if (device.name.isEmpty) {      return;    }    if (devices.any((dev) => dev.address == device.remoteId.toString())) {      return;    }    print('扫描到设备: ${device.name}, 地点: ${device.remoteId}');    var dev = BluetoothDeviceInfo(name: device.name, address: device.remoteId.toString());    devices.add(dev);  }  @override  void onConnectSuccess() {    print('毗连成功');  }  @override  void onDisconnected() {    print('断开毗连');    SnackBarManager.instance.showSnackBar("蓝牙断开毗连", "");    bluetoothInfo.value.isConnected = false;    for (var device in devices) {      device.isConnected = false;    }    bluetoothInfo.refresh();    devices.refresh();  }  @override  void onConnectFailed(String error) {    print('毗连失败: $error');  }  @override  void onDataReceived(List<int> data) {    print('收到数据: $data');    var receivedData = Ua200Receiver.getBleData(data);    if (receivedData != "") {      print('收到数据: $receivedData');      receivedMessage.value = '$receivedData\n${receivedMessage.value}';    }  }}
复制代码
onDataReceived接收的数据为byte数组,可根据自己BLE蓝牙协议进行剖析,此demo收发皆使用string转byte后进行通讯。

  • 实现效果

  • demo地点:https://gitee.com/hfyangi/bluetooth

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

美食家大橙子

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表