Flutter蓝牙通信插件flutter_ble_peripheral_central的使用

发布于 1周前 作者 itying888 来自 Flutter

Flutter蓝牙通信插件flutter_ble_peripheral_central的使用

1. 引言

该项目是探索跨平台应用间数据交换方法的一部分,通过蓝牙低功耗(BLE)实现。项目旨在使iOS和Android平台上的功能在Flutter中可用,通过参考alexanderlavrushko的BLEProof-collection(https://github.com/alexanderlavrushko/BLEProof-collection),创建了flutter_ble_peripheral_central插件项目。

该项目适用于最新的iOS和Android版本。对于简单的应用间数据交换,该插件已经足够。然而,对于更复杂的设备控制,我们建议使用经过验证和广泛使用的库。

请注意,UUIDs和功能列表直接采用了BLEProof-collection的内容。

2. 截图

主页 中央模式 外围模式

3. 设置

Android
  1. AndroidManifest.xml中添加权限。
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  1. 注册外围服务到AndroidManifest.xml
<service android:name="com.novice.flutter_ble_peripheral_central.ble.BlePeripheralService"
         android:exported="true"
         android:enabled="true"
         android:permission="android.permission.BLUETOOTH">
</service>
  1. 注册中央服务到AndroidManifest.xml
<service android:name="com.novice.flutter_ble_peripheral_central.ble.BleCentralService"
         android:exported="true"
         android:enabled="true"
         android:permission="android.permission.BLUETOOTH_ADMIN">
    <intent-filter>
        <action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
    </intent-filter>
</service>
iOS
  1. 对于iOS,必须在应用程序的Info.plist文件中添加以下条目。否则无法访问Core Bluetooth。
<key>NSBluetoothAlwaysUsageDescription</key>
<string>我们使用蓝牙来展示中央与外围之间的基本通信</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>我们使用蓝牙来展示中央与外围之间的基本通信</string>

4. 使用

import 'package:flutter_ble_peripheral_central/flutter_ble_peripheral_central.dart';

// 插件实例
final _flutterBlePeripheralCentralPlugin = FlutterBlePeripheralCentral();

// 获取平台版本
var platformVersion = await _flutterBlePeripheralCentralPlugin.getPlatformVersion();

// 外围模式
StreamSubscription<dynamic>? _eventSubscription;
var advertisingText = "广告数据";   // 只有iOS
var readableText = "可读数据";

// 开始广告
_eventSubscription = await _flutterBlePeripheralCentralPlugin.startBlePeripheralService(advertisingText, readableText).listen((event) {
  // 处理事件
  // ...
});

// 编辑可读文本
await _flutterBlePeripheralCentralPlugin.editTextCharForRead(readableText);

// 发送指示
var sendData = '发送数据';
var result = await _flutterBlePeripheralCentralPlugin.sendIndicate(sendData);

// 停止广告
await _flutterBlePeripheralCentralPlugin.stopBlePeripheralService();

// 中央模式
// 扫描并自动连接
_eventSubscription = await _flutterBlePeripheralCentralPlugin.scanAndConnect().listen((event) {
  // 处理事件
  // ...
});

// 断开连接
await _flutterBlePeripheralCentralPlugin.bleDisconnect();

// 读取特征值
var result = await _flutterBlePeripheralCentralPlugin.bleReadCharacteristic();

// 写入特征值
var sendData = '发送数据';
await _flutterBlePeripheralCentralPlugin.bleWriteCharacteristic(sendData);

示例代码

// example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ble_peripheral_central/flutter_ble_peripheral_central.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(MaterialApp(home: HomeScreen()));
}

// 主页面
class HomeScreen extends StatelessWidget {
  HomeScreen({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text(
            'BLE 外围 & 中央',
            style: TextStyle(fontSize: 27, fontWeight: FontWeight.bold,),
          )
      ),
      body: Column(
        children: [
          Text(
            '示例',
            style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold,),
          ),
          SizedBox(height: MediaQuery.of(context).size.height * 0.4),
          Center(
            child: Container(
              child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton.icon(
                      onPressed: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) {
                            return BLEPeripheralWidget();
                          }),
                        );
                      },
                      icon: Icon(
                          Icons.login_sharp,
                          size: MediaQuery.of(context).size.width * 0.07,
                          color: Colors.white
                      ),
                      label: AnimatedDefaultTextStyle(
                        duration: const Duration(milliseconds: 50),
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: MediaQuery.of(context).size.width * 0.05,
                          fontWeight: FontWeight.bold,
                        ),
                        child: Text('BLE 外围视图'),
                      ),
                      style: ElevatedButton.styleFrom(
                        primary: Colors.black,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(10),
                        ),
                        padding: EdgeInsets.symmetric(
                          vertical: MediaQuery.of(context).size.height * 0.02,
                          horizontal: MediaQuery.of(context).size.width * 0.08,
                        ),
                      ),
                    ),
                    SizedBox(height: 10,),
                    ElevatedButton.icon(
                      onPressed: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) {
                            return BLECentralWidget();
                          }),
                        );
                      },
                      icon: Icon(
                          Icons.login_sharp,
                          size: MediaQuery.of(context).size.width * 0.07,
                          color: Colors.white
                      ),
                      label: AnimatedDefaultTextStyle(
                        duration: const Duration(milliseconds: 50),
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: MediaQuery.of(context).size.width * 0.05,
                          fontWeight: FontWeight.bold,
                        ),
                        child: Text('BLE 中央视图      '),
                      ),
                      style: ElevatedButton.styleFrom(
                        primary: Colors.black,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(10),
                        ),
                        padding: EdgeInsets.symmetric(
                          vertical: MediaQuery.of(context).size.height * 0.02,
                          horizontal: MediaQuery.of(context).size.width * 0.08,
                        ),
                      ),
                    ),
                  ]
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// BLE 外围视图
class BLEPeripheralWidget extends StatefulWidget {
  [@override](/user/override)
  _BLEPeripheralWidgetState createState() => _BLEPeripheralWidgetState();
}

class _BLEPeripheralWidgetState extends State<BLEPeripheralWidget> {
  final _flutterBlePeripheralCentralPlugin = FlutterBlePeripheralCentral();

  List<String> _events = [];
  final _eventStreamController = StreamController<String>();

  final _bluetoothState = TextEditingController();
  final _advertisingText = TextEditingController();
  final _readableText = TextEditingController();
  final _indicateText = TextEditingController();
  final _writeableText = TextEditingController();

  bool _isSwitchOn = false;

  final ScrollController _scrollController = ScrollController();

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

  [@override](/user/override)
  void dispose() {
    _eventStreamController.close();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text(
            'BLE 外围视图',
            style: TextStyle(fontSize: 27, fontWeight: FontWeight.bold),
          )
      ),
      body: SingleChildScrollView(child: Column(
        children: [
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "蓝牙状态",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Container(
                      width: MediaQuery.of(context).size.width * 0.6,
                      height: 45,
                      child: TextField(
                        controller: _bluetoothState,
                        decoration: InputDecoration(
                          border: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey),
                          ),
                          contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                        ),
                        enabled: false,
                      ),
                    ),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "广告数据",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Container(
                            width: MediaQuery.of(context).size.width * 0.75,
                            height: 45,
                            child: TextField(
                              controller: _advertisingText,
                              decoration: InputDecoration(
                                border: OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.grey),
                                ),
                                contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                              ),
                              enabled: Platform.isIOS ? true : false,
                            ),
                          ),
                          SizedBox(width: 10),
                          Container(
                              width: MediaQuery.of(context).size.width * 0.17,
                              height: 45,
                              child: Transform.scale(
                                scale: 1.3,
                                child: Switch(
                                  value: _isSwitchOn,
                                  activeColor: CupertinoColors.activeBlue,
                                  onChanged: (value) {
                                    setState(() {
                                      _isSwitchOn = value;
                                    });

                                    if (_isSwitchOn) {
                                      _bleStartAdvertising(_advertisingText.text, _readableText.text);
                                    } else {
                                      _bleStopAdvertising();
                                    }
                                  },
                                ),
                              )
                          ),
                        ]),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "可读特性",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Container(
                            width: MediaQuery.of(context).size.width * 0.67,
                            height: 45,
                            child: TextField(
                              controller: _readableText,
                              decoration: InputDecoration(
                                border: OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.grey),
                                ),
                                contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                              ),
                            ),
                          ),
                          SizedBox(width: 10,),
                          Container(
                            width: MediaQuery.of(context).size.width * 0.25,
                            height: 45,
                            child:
                            ElevatedButton(
                              onPressed: () async {
                                _bleEditTextCharForRead(_readableText.text);
                              },
                              child: Text('编辑'),
                            ),
                          ),
                        ]
                    ),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "指示",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Container(
                            width: MediaQuery.of(context).size.width * 0.67,
                            height: 45,
                            child: TextField(
                              controller: _indicateText,
                              decoration: InputDecoration(
                                hintText: '输入指示值',
                                border: OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.grey),
                                ),
                                contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                              ),
                            ),
                          ),
                          SizedBox(width: 10,),
                          Container(
                            width: MediaQuery.of(context).size.width * 0.25,
                            height: 45,
                            child:
                            ElevatedButton(
                              onPressed: () async {
                                _bleIndicate(_indicateText.text);
                              },
                              child: Text('发送'),
                            ),
                          ),
                        ]
                    ),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "可写特性",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Container(
                      width: MediaQuery.of(context).size.width * 0.6,
                      height: 45,
                      child: TextField(
                        controller: _writeableText,
                        decoration: InputDecoration(
                          border: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey),
                          ),
                          contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                        ),
                        enabled: false,
                      ),
                    ),
                  ),
                ],
              )
          ),
          Container(
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Container(
                width: double.infinity,
                height: MediaQuery.of(context).size.height * 0.32,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey),
                ),
                child:
                ListView.builder(
                  controller: _scrollController,
                  itemCount: _events.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                        title: Text(
                          _events[index],
                          style: TextStyle(fontSize: 15,),
                        )
                    );
                  },
                ),
              ),
            ),
          ),
        ],
      ),
      ),
    );
  }

  StreamSubscription<dynamic>? _eventSubscription;

  void _bleStartAdvertising(String advertisingText, String readableText) async {
    _clearLog();
    _eventStreamController.sink.add('Starting...');

    _eventSubscription = await _flutterBlePeripheralCentralPlugin
        .startBlePeripheralService(advertisingText, readableText)
        .listen((event) {
      _eventStreamController.sink.add('-> ' + event);

      _addEvent(event);

      print('----------------------- > event: ' + event);
    });
  }

  void _bleStopAdvertising() async {
    await _flutterBlePeripheralCentralPlugin.stopBlePeripheralService();
  }

  void _bleEditTextCharForRead(String readableText) async {
    await _flutterBlePeripheralCentralPlugin.editTextCharForRead(readableText);
  }

  void _bleIndicate(String sendData) async {
    await _flutterBlePeripheralCentralPlugin.sendIndicate(sendData);
  }

  // 添加事件
  void _addEvent(String event) {
    setState(() {
      _events.add(event);
    });

    Map<String, dynamic> responseMap = jsonDecode(event);

    if (responseMap.containsKey('message')) {
      String message = responseMap['message'];
      print('Message: $message');
    } else if (responseMap.containsKey('state')) {
      setState(() {
        _bluetoothState.text = responseMap['state'];
      });

      if (event == 'disconnected') {
        _eventSubscription?.cancel();
      }
    } else if (responseMap.containsKey('onCharacteristicWriteRequest')) {
      setState(() {
        _writeableText.text = responseMap['onCharacteristicWriteRequest'];
      });
    } else {
      print('Message key not found in the JSON response.');
    }

    // 滚动到底部
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
  }

  // 清空日志
  void _clearLog() {
    setState(() {
      _events.clear();
    });
  }

  void _permissionCheck() async {
    if (Platform.isAndroid) {
      var permission = await Permission.location.request();
      var bleScan = await Permission.bluetoothScan.request();
      var bleConnect = await Permission.bluetoothConnect.request();
      var bleAdvertise = await Permission.bluetoothAdvertise.request();
      var locationWhenInUse = await Permission.locationWhenInUse.request();

      print('location permission: ${permission.isGranted}');
      print('bleScan permission: ${bleScan.isGranted}');
      print('bleConnect permission: ${bleConnect.isGranted}');
      print('bleAdvertise permission: ${bleAdvertise.isGranted}');
      print('location locationWhenInUse: ${locationWhenInUse.isGranted}');
    }
  }
}

// BLE 中央视图
class BLECentralWidget extends StatefulWidget {
  [@override](/user/override)
  _BLECentralWidgetState createState() => _BLECentralWidgetState();
}

class _BLECentralWidgetState extends State<BLECentralWidget> {
  final _flutterBlePeripheralCentralPlugin = FlutterBlePeripheralCentral();

  List<String> _events = [];
  final _eventStreamController = StreamController<String>();

  var _lifecycleState = TextEditingController();
  var _readableText = TextEditingController();
  var _writeableText = TextEditingController();
  var _indicateText = TextEditingController();

  bool _isSwitchOn = false;

  final ScrollController _scrollController = ScrollController();

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

  [@override](/user/override)
  void dispose() {
    _eventStreamController.close();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'BLE 中央视图',
          style: TextStyle(fontSize: 27, fontWeight: FontWeight.bold),
        ),
      ),
      body: SingleChildScrollView(child: Column(
        children: [
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "生命周期状态",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Container(
                      width: MediaQuery.of(context).size.width * 0.6,
                      height: 45,
                      child: TextField(
                        controller: _lifecycleState,
                        decoration: InputDecoration(
                          border: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey),
                          ),
                          contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                        ),
                        enabled: false,
                      ),
                    ),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  SizedBox(
                    width: double.infinity,
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Container(
                            width: MediaQuery.of(context).size.width * 0.75,
                            height: 45,
                            child: Padding(padding: EdgeInsets.symmetric(vertical: 8,),
                              child: Text(
                                '扫描 & 自动连接',
                                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                              ),),
                          ),
                          SizedBox(width: 10),
                          Container(
                              width: MediaQuery.of(context).size.width * 0.17,
                              height: 45,
                              child: Transform.scale(
                                scale: 1.3,
                                child: Switch(
                                  value: _isSwitchOn,
                                  activeColor: CupertinoColors.activeBlue,
                                  onChanged: (value) {
                                    setState(() {
                                      _isSwitchOn = value;
                                    });

                                    if (_isSwitchOn) {
                                      _bleScanAndConnect();
                                    } else {
                                      _bleDisconnect();
                                    }
                                  },
                                ),
                              )
                          ),
                        ]),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "可读特性",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Container(
                            width: MediaQuery.of(context).size.width * 0.67,
                            height: 45,
                            child: TextField(
                              controller: _readableText,
                              decoration: InputDecoration(
                                border: OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.grey),
                                ),
                                contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                              ),
                              enabled: false,
                            ),
                          ),
                          SizedBox(width: 10,),
                          Container(
                            width: MediaQuery.of(context).size.width * 0.25,
                            height: 45,
                            child:
                            ElevatedButton(
                              onPressed: () async {
                                _bleReadCharacteristic();
                              },
                              child: Text('读取'),
                            ),
                          ),
                        ]
                    ),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "可写特性",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.start,
                        children: [
                          Container(
                            width: MediaQuery.of(context).size.width * 0.67,
                            height: 45,
                            child: TextField(
                              controller: _writeableText,
                              decoration: InputDecoration(
                                hintText: '写入发送数据',
                                border: OutlineInputBorder(
                                  borderSide: BorderSide(color: Colors.grey),
                                ),
                                contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                              ),
                            ),
                          ),
                          SizedBox(width: 10,),
                          Container(
                            width: MediaQuery.of(context).size.width * 0.25,
                            height: 45,
                            child:
                            ElevatedButton(
                              onPressed: () async {
                                _bleWriteCharacteristic(_writeableText.text);
                              },
                              child: Text('写入'),
                            ),
                          ),
                        ]
                    ),
                  ),
                ],
              )
          ),
          Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(
                    "指示",
                    style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 5),
                  SizedBox(
                    width: double.infinity,
                    child: Container(
                      width: MediaQuery.of(context).size.width * 0.6,
                      height: 45,
                      child: TextField(
                        controller: _indicateText,
                        decoration: InputDecoration(
                          border: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey),
                          ),
                          contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 12,),
                        ),
                        enabled: false,
                      ),
                    ),
                  ),
                ],
              )
          ),
          Container(
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10,),
              child: Container(
                width: double.infinity,
                height: MediaQuery.of(context).size.height * 0.32,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey),
                ),
                child:
                ListView.builder(
                  controller: _scrollController,
                  itemCount: _events.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                        title: Text(
                          _events[index],
                          style: TextStyle(fontSize: 15,),
                        )
                    );
                  },
                ),
              ),
            ),
          ),
        ],
      ),
      ),
    );
  }

  StreamSubscription<dynamic>? _eventSubscription;

  void _bleScanAndConnect() async {
    _clearLog();
    _eventStreamController.sink.add('Starting...');

    _eventSubscription = await _flutterBlePeripheralCentralPlugin
        .scanAndConnect()
        .listen((event) {
      _eventStreamController.sink.add('-> ' + event);

      _addEvent(event);

      print('----------------------- > event: ' + event);
    });
  }

  void _bleReadCharacteristic() async {
    var result = await _flutterBlePeripheralCentralPlugin.bleReadCharacteristic();
    setState(() {
      _readableText.text = result!;
    });
  }

  void _bleWriteCharacteristic(String sendData) async {
    await _flutterBlePeripheralCentralPlugin.bleWriteCharacteristic(sendData);
  }

  void _bleDisconnect() async {
    await _flutterBlePeripheralCentralPlugin.bleDisconnect();
  }

  // 添加事件
  void _addEvent(String event) {
    setState(() {
      _events.add(event);
    });

    Map<String, dynamic> responseMap = jsonDecode(event);

    if (responseMap.containsKey('message')) {
      String message = responseMap['message'];
      print('Message: $message');
    } else if (responseMap.containsKey('state')) {
      setState(() {
        _lifecycleState.text = responseMap['state'];
      });
      if (event == 'disconnected') {
        _eventSubscription?.cancel();
      }
    } else if (responseMap.containsKey('onCharacteristicChanged')) {
      setState(() {
        _indicateText.text = responseMap['onCharacteristicChanged'];
      });
    } else {
      print('Message key not found in the JSON response.');
    }

    // 滚动到底部
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
  }

  // 清空日志
  void _clearLog() {
    setState(() {
      _events.clear();
    });
  }

  void _permissionCheck() async {
    if (Platform.isAndroid) {
      var permission = await Permission.location.request();
      var bleScan = await Permission.bluetoothScan.request();
      var bleConnect = await Permission.bluetoothConnect.request();
      var bleAdvertise = await Permission.bluetoothAdvertise.request();
      var locationWhenInUse = await Permission.locationWhenInUse.request();

      print('location permission: ${permission.isGranted}');
      print('bleScan permission: ${bleScan.isGranted}');
      print('bleConnect permission: ${bleConnect.isGranted}');
      print('bleAdvertise permission: ${bleAdvertise.isGranted}');
      print('location locationWhenInUse: ${locationWhenInUse.isGranted}');
    }
  }
}

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

1 回复

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


当然,下面是一个关于如何在Flutter应用中使用flutter_ble_peripheral_central插件进行蓝牙通信的示例代码。这个插件允许你的Flutter应用同时作为蓝牙外设(Peripheral)和中心设备(Central)进行操作。

添加依赖

首先,你需要在你的pubspec.yaml文件中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_ble_peripheral_central: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

配置权限

在Android上,你需要在AndroidManifest.xml中添加必要的权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.yourapp">
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <!-- 其他权限 -->
    <!-- ... -->
</manifest>

在iOS上,你需要在Info.plist中添加蓝牙使用描述:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>App needs bluetooth to connect to devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>App needs bluetooth to connect to devices</string>

外设(Peripheral)模式示例

下面是一个简单的外设模式示例,它启动一个服务并广播一个特征值:

import 'package:flutter/material.dart';
import 'package:flutter_ble_peripheral_central/flutter_ble_peripheral.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  FlutterBlePeripheral _peripheral = FlutterBlePeripheral();

  @override
  void initState() {
    super.initState();
    _initPeripheral();
  }

  Future<void> _initPeripheral() async {
    // 定义服务
    Uuid serviceUuid = Uuid.parse("0000180d-0000-1000-8000-00805f9b34fb");
    Uuid characteristicUuid = Uuid.parse("00002a37-0000-1000-8000-00805f9b34fb");

    // 创建服务和特征值
    FlutterBlePeripheralService service = FlutterBlePeripheralService(
      serviceUuid: serviceUuid,
      characteristics: [
        FlutterBlePeripheralCharacteristic(
          characteristicUuid: characteristicUuid,
          properties: [
            CharacteristicProperty.read,
            CharacteristicProperty.notify,
          ],
          value: Uint8List.fromList([0x00]),
        ),
      ],
    );

    // 添加服务并开始广播
    await _peripheral.addService(service);
    await _peripheral.startAdvertising();
    await _peripheral.setNotifyValue(characteristicUuid, Uint8List.fromList([0x01]));

    print("Peripheral started and advertising");
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter BLE Peripheral Example'),
        ),
        body: Center(
          child: Text('BLE Peripheral is running'),
        ),
      ),
    );
  }
}

中心(Central)模式示例

下面是一个简单的中心模式示例,它扫描设备并连接到第一个找到的设备:

import 'package:flutter/material.dart';
import 'package:flutter_ble_peripheral_central/flutter_ble_central.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  FlutterBleCentral _central = FlutterBleCentral();

  @override
  void initState() {
    super.initState();
    _scanForDevices();
  }

  Future<void> _scanForDevices() async {
    _central.scanForDevices(withServices: []).listen((scanResult) async {
      print("Device found: ${scanResult.device.id}");

      // 连接第一个找到的设备
      if (scanResult.device.name != null) {
        await _central.connectToDevice(scanResult.device.id);

        // 列出服务
        List<FlutterBleCentralService> services = await _central.discoverServices(scanResult.device.id);
        services.forEach((service) {
          print("Service found: ${service.uuid}");

          // 列出特征值
          _central.discoverCharacteristics(scanResult.device.id, service.uuid).then((characteristics) {
            characteristics.forEach((characteristic) {
              print("Characteristic found: ${characteristic.uuid}");
            });
          });
        });

        // 停止扫描
        _central.stopScan();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter BLE Central Example'),
        ),
        body: Center(
          child: Text('Scanning for BLE devices...'),
        ),
      ),
    );
  }
}

注意事项

  1. 权限处理:在实际应用中,你需要处理权限请求,尤其是在iOS上,蓝牙权限和位置权限是紧密相关的。
  2. 错误处理:上述代码未包含错误处理逻辑,实际应用中应添加适当的错误处理。
  3. 平台差异:不同平台(Android和iOS)在蓝牙处理上可能有细微差异,请根据实际需要进行调整。

这段代码提供了一个基本的框架,展示了如何使用flutter_ble_peripheral_central插件进行蓝牙通信。根据具体需求,你可以进一步扩展和优化这些代码。

回到顶部