蓝牙通信详解


title: 蓝牙通信
author: wucm
tags:

  • Android
    categories:
  • Android

Android手机间通过蓝牙方式进行通信,有两种常见的方式,一种是socket方式,另一种是通过Gatt Server(Android 5.0以后)通信,socket方式最为简单,但是很多低功耗的蓝牙设备,如单片机上的蓝牙模块可能不支持;而Gatt方式相对比较复杂。其实无论是socket方式还是Gatt,Android设备间蓝牙通信都是一种C/S(client-server)模式。
本文基于两种通信方式,进行详细展开,并推荐了开源项目,建议配合学习。

前置知识

特征

蓝牙4.0是以参数来进行数据传输的,即服务端定好一个参数,客户端可以对这个参数进行读,写,通知等操作,这个东西我们称之为特征值(characteristic)。比如我们这个特征值是电量的值,另一个特征值是设备读取的温度值。BLE 主从机的通信均是通过 Characteristic 来实现。每个特征有唯一UUID。

属性

包含 特征的声明(Characteristic Declaration)条目、特征的值(Characteristic Value)条目。一个特性至少包含2个属性条目(也即属性值必须要有,而描述符根据需要可选):一个属性条目用于声明(Characteristic Declaration),一个属性条目用于存放特性的值(Characteristic Value)。

  • Read: 读属性。具有这个属性的特征是可读的,也就是说这个属性允许手机来读取一些信息,手机可以发送指令来读取某个具有读属性UUID的信息。
  • Notify: 通知属性。具有这个属性的特征是可以发送通知的,也就是说具有这个属性的特征可以主动发送信息给手机。
  • Write: 写属性。具有这个属性的特征是可以接收写入数据的,通常手机发送数据给蓝模块就是通过这个属性完成的。这个属性在Write 完成后,会发送写入完成结果的反馈给手机,然后手机再写入下一包或处理后续业务。这个属性在写入一包数据后,需要等待应用层返回写入结果,速度比较慢。
  • WriteWithout Response:写属性。从字面意思上看,只是写,不需要返回写的结果,这个属性的特点是不需要应用层返回,完全依靠协议层完成,速度快,但是写入速度超过协议处理速度的时候会丢包。

描述符

包含 特征的客户配置(Client Characteristic Configuration)条目、特征的用户描述(Characteristic User Description)条目。描述符是一个额外的属性以提供更多特性的信息,它提供一个人类可识别的特性描述的实例。每个描述符有唯一UUID。

服务

特征值有很多,我们需要对特征值进行分类,分出来的类我们称之为服务(service)。每个服务有唯一UUID。

GATT

GATT(Generic Attribute Profile):中文名叫通用属性协议,它定义了services和characteristic两种东西来完成低功耗蓝牙设备之间的数据传输。它是建立在通用数据协议Attribute Protocol (ATT)之上的,ATT把services和characteristic以及相关的数据保存在一张简单的查找表中,该表使用16-bit的id作为索引。

profile

profile可以理解为一种规范,一个标准的通信协议,它存在于从机中。蓝牙组织规定了一些标准的profile,例如 HID OVER GATT ,防丢器 ,心率计等。每个profile中会包含多个service,每个service代表从机的一种能力。

开发流程

下面说一下GATT的实现,也就是我们常说的BLE蓝牙开发,这种方式也可以运行在传统蓝牙上。

1.申请权限

这里不讲如何动态申请权限,如果你想轻装上阵,声明权限后,自己手动去授权即可

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--目标设备版本是 Android9 及其以上的系统上,需要申请此权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--目标设备版本是 Android12 及其以上的系统上,扫描蓝牙设备需要申请此权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!--目标设备版本是 Android12 及其以上的系统上,使当前设备可被其他蓝牙设备检测到需要申请此权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!--目标设备版本是 Android12 及其以上的系统上,与已配对的蓝牙设备通信需要申请此权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

2.判断是否支持蓝牙功能

我们可以通过 getDefaultAdapter 来获取该实例。

private BluetoothAdapter bleAdapter = BluetoothAdapter.getDefaultAdapter();

假如 bleAdapternull 就意味着设备不支持蓝牙。

3.打开蓝牙

private void openBle() {
    //通过isEnable() 判断蓝牙是否打开
    if (!bleAdapter.isEnabled()) {
        //如果没有打开,打开蓝牙
        if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
            return;
        }
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
}

在上面的内容都准备好了,那就可以开始进入核心流程了。

4.开始扫描

private BluetoothLeScanner bleScanner = bleAdapter.getBluetoothLeScanner();

private void startScan() {
    if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_SCAN) != PERMISSION_GRANTED) {
        return;
    }
    bleScanner.startScan(new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            super.onScanResult(callbackType, result);
            //连接目标设备
            connectDevice(result);
        }
    });
}

5.连接目标设备

扫描到设备后,找到目标设备就可以开始连接了。

private BluetoothGatt bluetoothGatt;

private void connectDevice(ScanResult result) {
    BluetoothDevice device = result.getDevice();
    if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED
        && !device.getName().matches("蓝牙通配符")) {
        return;
    }
    bluetoothGatt = device.connectGatt(this, true, new BluetoothGattCallback() {
        //Gatt连接状态回调
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            //连接目标设备成功
            if (newState == BluetoothGatt.STATE_CONNECTED) {
                //搜索服务
                discoverServices(gatt);
            }
        }
    });
}

BluetoothGatt可以理解为与其他设备沟通交流的对象。

6.搜索服务

private void discoverServices(BluetoothGatt gatt) {
    if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
        return;
    }
    //搜索服务
    gatt.discoverServices();
}

好了,现在我们看看执行完 bluetoothGatt.discoverServices() 又发生了什么变化吧。

//BluetoothGattCallback.java
//发现服务回调
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);
    //服务发现结束
    if (status == BluetoothGatt.GATT_SUCCESS) {
        findNotifyCharacteristics(gatt);
    }
}

7.打开通知通道

假设已经跟硬件端确认好了所需信息(服务 uuid,特征uuid*,描述符 uuid ),这时,确实可以开始写入消息。但是,我们却得不到回应,因为我们没有打开通知通道。

开启监听,即建立与设备的通信的数据通道,BLE开发中只有当上位机成功开启监听后才能与下位机收发数据。开启监听的方式如下:

//获取到该设备的【通知通道】的特征值
private void findNotifyCharacteristics(BluetoothGatt gatt) {
    List<BluetoothGattCharacteristic> notifyCharacteristics = new ArrayList<>();
    for (BluetoothGattService service : gatt.getServices()) {
        for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
            //两个都是通知的意思,notify和indication的区别在于
            //notify只是将你要发的数据发送给手机,没有确认机制,不会保证数据发送是否到达。
            //而indication的方式在手机收到数据时会主动回一个ack回来。即有确认机制,只有收到这个ack你才能继续发送下一个数据。这保证了数据的正确到达,也起到了流控的作用。所以在打开通知的时候,需要设置一下。
            if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0
                || (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
                notifyCharacteristics.add(characteristic);
            }
        }
    }

    for (BluetoothGattCharacteristic notifyCharacteristic : notifyCharacteristics) {
        //打开通知通道
        openNotifyChannel(gatt, notifyCharacteristic, true);
    }
}
//打开通知通道
private void openNotifyChannel(BluetoothGatt gatt, BluetoothGattCharacteristic notifyCharacteristic, boolean enabled) {
    if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
        return;
    }
    //step1:打开通知通道
    gatt.setCharacteristicNotification(notifyCharacteristic, enabled);
    List<BluetoothGattDescriptor> descriptors = notifyCharacteristic.getDescriptors();
    if (descriptors.isEmpty()) {
        return;
    }
    for (BluetoothGattDescriptor descriptor : descriptors) {
        if (descriptor == null) {
            break;
        }
        //step2:为描述符设置对应的属性
        if ((notifyCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
            descriptor.setValue(enabled ? ENABLE_NOTIFICATION_VALUE : DISABLE_NOTIFICATION_VALUE);
        } else if ((notifyCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
            descriptor.setValue(enabled ? ENABLE_INDICATION_VALUE : DISABLE_NOTIFICATION_VALUE);
        }
        //step3:向设备写入上述的描述符
        gatt.writeDescriptor(descriptor);
    }
}

打开通知通道就可以写入数据了👇

8.写入数据

private void write(BluetoothGatt gatt) {
    if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
        return;
    }
    //具体用哪个服务特征,需要跟硬件端确认
    UUID serviceUUID= UUID.fromString(SERVIC_UUID);
    UUID writerUUID = UUID.fromString(WRITER_UUID);
    //step1:获取到写通道的特征值
    BluetoothGattCharacteristic characteristic = gatt.getService(serviceUUID).getCharacteristic(writerUUID);
    //step2:把我们要发送的信息存在上述特征值中
    characteristic.setValue("QCoder,U are so handsome.".getBytes());
    //step3:向设备写入上面的特征值
    gatt.writeCharacteristic(characteristic);
}

这样,对方就收到我们的消息了。如果你发的消息,对方想回复(提前协商好的指令)。那么我们将会在下面这个方法中收到

//BluetoothGattCallback.java
//特性改变回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    super.onCharacteristicChanged(gatt, characteristic);
    //拿到传回来的原始数据 byte数组
    byte[] content = characteristic.getValue();
}

9.读取数据

private void read(BluetoothGatt gatt) {
    //具体用哪个服务特征,需要跟硬件端确认
    UUID serviceUUID= UUID.fromString(SERVIC_UUID);
    UUID readerUUID = UUID.fromString(READER_UUID);
    BluetoothGattCharacteristic characteristic = gatt.getService(serviceUUID).getCharacteristic(readerUUID);
    if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
        return;
    }
    gatt.readCharacteristic(characteristic);
}

读取的特征值将通过下面方法收到

//BluetoothGattCallback.java
//特性读取回调
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    super.onCharacteristicRead(gatt, characteristic, status);
    //拿到传回来的原始数据 byte数组
    byte[] content = characteristic.getValue();
}

10.分包

1.设置MTU(单次数据包的最大字节数),值越大传输效率越高,但相对的延迟也高,如果传输大数据可以设置的大一点。

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        gatt.requestMtu(517); // 请求最大MTU值
    }
}

@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        // MTU修改成功,获取实际的MTU值。个值可能是请求的值,也可能是设备支持的最大值
        actualMtu = mtu;
    } 
}

2.发送时根据MTU进行分包,并在每个数据包的前几个字节添加总包数和当前包数。

public List<byte[]> splitData(byte[] data, int mtu) {
    int headerSize = 4; // 总包数和当前包数占用4个字节
    int maxPacketSize = mtu - headerSize;
    List<byte[]> packets = new ArrayList<>();
    int totalPackets = (data.length + maxPacketSize - 1) / maxPacketSize;
    
    for (int i = 0; i < totalPackets; i++) {
        int start = i * maxPacketSize;
        int length = Math.min(maxPacketSize, data.length - start);
        byte[] packet = new byte[headerSize + length];
        
        // 添加总包数
        packet[0] = (byte) ((totalPackets >> 8) & 0xFF);
        packet[1] = (byte) (totalPackets & 0xFF);
        
        // 添加当前包数
        packet[2] = (byte) ((i >> 8) & 0xFF);
        packet[3] = (byte) (i & 0xFF);
        
        // 添加数据
        System.arraycopy(data, start, packet, headerSize, length);
        packets.add(packet);
    }
    return packets;
}

3.接收时进行合包

public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    byte[] value = characteristic.getValue();
    int totalPackets = (value[0] << 8) | (value[1] & 0xFF);
    int currentPacket = (value[2] << 8) | (value[3] & 0xFF);
    byte[] data = new byte[value.length - 4];
    System.arraycopy(value, 4, data, 0, data.length);

    // 判断重组数据是否完成
    // ...
}

补充

BluetoothGattCallback方法介绍

方法描述
onPhyUpdate物理层改变回调
onPhyRead设备物理层读取回调
onConnectionStateChangeGatt连接状态回调
onServicesDiscovered发现服务回调
onCharacteristicRead特性读取回调
onCharacteristicWrite特性写入回调
onCharacteristicChanged特性改变回调
onDescriptorRead描述读取回调
onDescriptorWrite描述写入回调
onReliableWriteCompleted可靠写入完成回调
onReadRemoteRssi读取远程设备信号值回调
onMtuChangedMtuSize改变回调
onConnectionUpdated连接更新回调
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值