Flutter开发之蓝牙链接传输数据

本文使用的是flutter_blue_plus插件来实现链接蓝牙之后,和设备直接实现数据互相传输的功能。

1、配置蓝牙权限

iOS权限设置

    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>App需要您的同意,才能访问蓝牙,进行设备连接,数据通讯服务</string>
    <key>NSBluetoothPeripheralUsageDescription</key>
    <string>App需要您的同意,才能访问蓝牙,进行设备连接,数据通讯服务</string>


Android权限设置

    <!-- 蓝牙-->
    <!-- google play store需要-->
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="false" />

    <!--    Android 12-->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <!--    Android 11 及以下-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION " />

2、添加flutter_blue_plus插件

flutter_blue_plus: ^1.31.8

3、搜索蓝牙设备列表页面,如图:

image.png

代码如下:

import 'dart:async';
import 'dart:io';

import 'package:demo/view/device_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';

class BluetoothPage extends StatefulWidget {
  const BluetoothPage({super.key});

  @override
  State<BluetoothPage> createState() => _BluetoothPageState();
}

class _BluetoothPageState extends State<BluetoothPage> {
  ///当前已经连接的蓝牙设备
  List<BluetoothDevice> _systemDevices = [];

  ///扫描到的蓝牙设备
  List<ScanResult> _scanResults = [];

  late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
  late StreamSubscription<bool> _isScanningSubscription;

  @override
  void initState() {
    super.initState();
    _scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) {
      _scanResults = results;
      if (mounted) {
        setState(() {});
      }
    }, onError: (error) {
      print('Scan Error:$error');
    });
    _isScanningSubscription = FlutterBluePlus.isScanning.listen((state) {
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    _scanResultsSubscription.cancel();
    _isScanningSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("蓝牙"),
      ),
      body: ListView(
        children: [
          ..._buildSystemDeviceTiles(),
          ..._buildScanResultTiles(),
        ],
      ),
      floatingActionButton: FlutterBluePlus.isScanningNow
          ? FloatingActionButton(
              onPressed: () {
                FlutterBluePlus.stopScan();
              },
              backgroundColor: Colors.red,
              child: const Text("Stop"),
            )
          : FloatingActionButton(
              onPressed: () async {
                try {
                  _systemDevices = await FlutterBluePlus.systemDevices;
                  print('dc-----_systemDevices$_systemDevices');
                } catch (e) {
                  print("Stop Scan Error:$e");
                }
                try {
                  // android is slow when asking for all advertisements,
                  // so instead we only ask for 1/8 of them
                  int divisor = Platform.isAndroid ? 8 : 1;
                  await FlutterBluePlus.startScan(
                      timeout: const Duration(seconds: 15),
                      continuousUpdates: true,
                      continuousDivisor: divisor);
                } catch (e) {
                  print("Stop Scan Error:$e");
                }
                if (mounted) {
                  setState(() {});
                }
              },
              child: const Text("SCAN"),
            ),
    );
  }

  List<Widget> _buildSystemDeviceTiles() {
    return _systemDevices.map((device) {
      return ListTile(
        title: Text(device.platformName),
        subtitle: Text(device.remoteId.toString()),
        trailing: ElevatedButton(
          onPressed: () {
            Get.to(DeviceScreen(device: device));
          },
          child: const Text('CONNECT'),
        ),
      );
    }).toList();
  }

  List<Widget> _buildScanResultTiles() {
    return _scanResults
        .map(
          (scanResult) => ListTile(
            title: Text(
              scanResult.device.platformName,
              overflow: TextOverflow.ellipsis,
            ),
            subtitle: Text(
              scanResult.device.remoteId.toString(),
            ),
            trailing: ElevatedButton(
              onPressed: () {
                Get.to(DeviceScreen(device: scanResult.device));
              },
              child: const Text('CONNECT'),
            ),
          ),
        )
        .toList();
  }
}

4、点击CONNECT进入当前设备详情页面,如图:

image.png

代码如下

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/route_manager.dart';

class DeviceScreen extends StatefulWidget {
  final BluetoothDevice device;
  const DeviceScreen({
    super.key,
    required this.device,
  });

  @override
  State<DeviceScreen> createState() => _DeviceScreenState();
}

class _DeviceScreenState extends State<DeviceScreen> {
  List<BluetoothService> _services = [];
  BluetoothConnectionState _connectionState =
      BluetoothConnectionState.disconnected;
  late StreamSubscription<BluetoothConnectionState>
      _connectionStateSubscription;

  bool get isConnected {
    return _connectionState == BluetoothConnectionState.connected;
  }

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

    _connectionStateSubscription =
        widget.device.connectionState.listen((state) async {
      _connectionState = state;
      if (state == BluetoothConnectionState.connected) {
        _services = []; // must rediscover services
      }
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    _connectionStateSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          widget.device.platformName,
        ),
        actions: [
          TextButton(
            onPressed: isConnected
                ? () async {
                    await widget.device.disconnect();
                  }
                : () async {
                    await widget.device.connect();
                    print("Connect: Success");
                  },
            child: Text(
              isConnected ? "DISCONNECT" : "CONNECT",
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          ListTile(
            title: Text(
              'Device is ${_connectionState.toString().split('.')[1]}.',
            ),
            trailing: TextButton(
              onPressed: () async {
                if (!isConnected) {
                  Get.snackbar('title', '请先连接蓝牙设备');
                  return;
                }
                _services = await widget.device.discoverServices();
                setState(() {});
              },
              child: const Text("Get Services"),
            ),
          ),
          ..._buildServiceTiles(),
        ],
      ),
    );
  }

  List<Widget> _buildServiceTiles() {
    return _services.map(
      (service) {
        return ExpansionTile(
          title: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              const Text('Service', style: TextStyle(color: Colors.blue)),
              Text(
                '0x${service.uuid.str.toUpperCase()}',
              ),
            ],
          ),
          children: service.characteristics.map((c) {
            return CharacteristicTile(
              characteristic: c,
              descriptorTiles: c.descriptors
                  .map((d) => DescriptorTile(descriptor: d))
                  .toList(),
            );
          }).toList(),
        );
      },
    ).toList();
  }
}

5、点击右上角的CONNECT链接上设备之后,获取service,如图:

image.png

代码如下:


class CharacteristicTile extends StatefulWidget {
  final BluetoothCharacteristic characteristic;
  final List<DescriptorTile> descriptorTiles;

  const CharacteristicTile(
      {Key? key, required this.characteristic, required this.descriptorTiles})
      : super(key: key);

  @override
  State<CharacteristicTile> createState() => _CharacteristicTileState();
}

class _CharacteristicTileState extends State<CharacteristicTile> {
  List<int> _value = [];

  late StreamSubscription<List<int>> _lastValueSubscription;

  @override
  void initState() {
    super.initState();
    _lastValueSubscription =
        widget.characteristic.lastValueStream.listen((value) {
      _value = value;
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    _lastValueSubscription.cancel();
    super.dispose();
  }

  BluetoothCharacteristic get c => widget.characteristic;

  List<int> _getRandomBytes() {
    // 将字符串转换为字节数组
    String data = 'Hello, Bluetooth!';
    List<int> bytes = utf8.encode(data);
    return bytes;
  }

  Future onReadPressed() async {
    try {
      await c.read();
      print("Read: Success");
    } catch (e) {
      print("Read Error:");
    }
  }

  Future onWritePressed() async {
    try {
      await c.write(_getRandomBytes(),
          withoutResponse: c.properties.writeWithoutResponse);
      print("Write: Success");
      if (c.properties.read) {
        await c.read();
      }
    } catch (e) {
      print("Write Error:");
    }
  }

  Future onSubscribePressed() async {
    try {
      String op = c.isNotifying == false ? "Subscribe" : "Unubscribe";
      await c.setNotifyValue(c.isNotifying == false);
      print("$op : Success");
      if (c.properties.read) {
        await c.read();
      }
      if (mounted) {
        setState(() {});
      }
    } catch (e) {
      print("Subscribe Error:");
    }
  }

  Widget buildUuid(BuildContext context) {
    String uuid = '0x${widget.characteristic.uuid.str.toUpperCase()}';
    return Text(uuid);
  }

  Widget buildValue(BuildContext context) {
    String data = _value.toString();
    return Text(
      data,
      style: const TextStyle(fontSize: 13),
    );
  }

  Widget buildReadButton(BuildContext context) {
    return TextButton(
        child: const Text(
          "Read",
          style: TextStyle(fontSize: 13),
        ),
        onPressed: () async {
          await onReadPressed();
          if (mounted) {
            setState(() {});
          }
        });
  }

  Widget buildWriteButton(BuildContext context) {
    bool withoutResp = widget.characteristic.properties.writeWithoutResponse;
    return TextButton(
        child: Text(withoutResp ? "WriteNoResp" : "Write",
            style: const TextStyle(fontSize: 13, color: Colors.grey)),
        onPressed: () async {
          await onWritePressed();
          if (mounted) {
            setState(() {});
          }
        });
  }

  Widget buildSubscribeButton(BuildContext context) {
    bool isNotifying = widget.characteristic.isNotifying;
    return TextButton(
        child: Text(isNotifying ? "Unsubscribe" : "Subscribe"),
        onPressed: () async {
          await onSubscribePressed();
          if (mounted) {
            setState(() {});
          }
        });
  }

  Widget buildButtonRow(BuildContext context) {
    bool read = widget.characteristic.properties.read;
    bool write = widget.characteristic.properties.write;
    bool notify = widget.characteristic.properties.notify;
    bool indicate = widget.characteristic.properties.indicate;
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (read) buildReadButton(context),
        if (write) buildWriteButton(context),
        if (notify || indicate) buildSubscribeButton(context),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return ExpansionTile(
      title: ListTile(
        title: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            const Text(
              'Characteristic',
              style: TextStyle(fontSize: 13),
            ),
            buildUuid(context),
            buildValue(context),
          ],
        ),
        subtitle: buildButtonRow(context),
        contentPadding: const EdgeInsets.all(0.0),
      ),
      children: widget.descriptorTiles,
    );
  }
}

class DescriptorTile extends StatefulWidget {
  final BluetoothDescriptor descriptor;

  const DescriptorTile({Key? key, required this.descriptor}) : super(key: key);

  @override
  State<DescriptorTile> createState() => _DescriptorTileState();
}

class _DescriptorTileState extends State<DescriptorTile> {
  List<int> _value = [];

  late StreamSubscription<List<int>> _lastValueSubscription;

  @override
  void initState() {
    super.initState();
    _lastValueSubscription = widget.descriptor.lastValueStream.listen((value) {
      _value = value;
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    _lastValueSubscription.cancel();
    super.dispose();
  }

  BluetoothDescriptor get d => widget.descriptor;

  List<int> _getRandomBytes() {
    // 将字符串转换为字节数组
    String data = 'Hello, Bluetooth!';
    return utf8.encode(data);
  }

  Future onReadPressed() async {
    try {
      await d.read();
      print("Descriptor Read : Success");
    } catch (e) {
      print("Descriptor Read Error:");
    }
  }

  Future onWritePressed() async {
    try {
      await d.write(_getRandomBytes());
      print("Descriptor Write : Success");
    } catch (e) {
      print("Descriptor Write Error:");
    }
  }

  Widget buildUuid(BuildContext context) {
    String uuid = '0x${widget.descriptor.uuid.str.toUpperCase()}';
    return Text(
      uuid,
      style: TextStyle(fontSize: 13),
    );
  }

  Widget buildValue(BuildContext context) {
    String data = _value.toString();
    return Text(
      data,
      style: TextStyle(fontSize: 13),
    );
  }

  Widget buildReadButton(BuildContext context) {
    return TextButton(
      child: Text("Read"),
      onPressed: onReadPressed,
    );
  }

  Widget buildWriteButton(BuildContext context) {
    return TextButton(
      child: Text("Write"),
      onPressed: onWritePressed,
    );
  }

  Widget buildButtonRow(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        buildReadButton(context),
        buildWriteButton(context),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          const Text(
            'Descriptor',
            style: TextStyle(fontSize: 13),
          ),
          buildUuid(context),
          buildValue(context),
        ],
      ),
      subtitle: buildButtonRow(context),
    );
  }
}

6、展开service,Write就是给设备发送数据,read就是读取设备传递过来的数据,如图:

image.png

发送数据的代码:

Future onWritePressed() async {
    try {
      await c.write(_getRandomBytes(),
          withoutResponse: c.properties.writeWithoutResponse);
      print("Write: Success");
      if (c.properties.read) {
        await c.read();
      }
    } catch (e) {
      print("Write Error:");
    }
  }

  List<int> _getRandomBytes() {
    // 将字符串转换为字节数组
    String data = 'Hello, Bluetooth!';
    List<int> bytes = utf8.encode(data);
    return bytes;
  }

更多具体的功能可以自行查看flutter_blue_plus给出的官方demo代码

CSDN地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容