【Android】蓝牙开发入门笔记

开头语

该文章是我在学习蓝牙开发后自己总结出来的一些常见的方法。由于是笔记,因此可能会缺少一些细节说明,大体上仅记录一下思路。

文章目录

    • 开头语
    • 1. 经典蓝牙
      • 服务端
        • 开启服务端
        • 和客户端通信
        • 关闭
      • 客户端
        • 扫描服务端
        • 连接服务端
        • 和服务端通信
        • 关闭
    • 2. 低功耗蓝牙BLE
      • 基础知识
        • Gatt
        • Service
        • Characteristics
      • 服务端
        • 开启服务端
        • 开启广播
          • 广播设置
          • 广播内容
          • 扫描回复包
        • 新增服务
        • 新增特性
        • 回复客户端请求
        • 主动通知客户端
        • 关闭服务端
      • 客户端
        • 扫描服务端
        • 连接服务端
        • 查询服务
        • 发送读特性请求
        • 发送写特性请求
        • 注册特性通知
        • 关闭客户端
    • 结束语

1. 经典蓝牙

服务端

开启服务端

开启服务端需要调用BluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord()

方法接收两个参数,关键参数为uuid。客户端连接到服务端时,需要知道uuid才能正确的连接上服务端。

/**
 * 创建一个经典蓝牙服务端
 * @param name 这个参数暂时不清楚有什么意义,可随意传
 * @param uuid 该参数需要客户端一致才能连接到服务端
*/
public BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord(String name, UUID uuid)
        throws IOException {
    return createNewRfcommSocketAndRecord(name, uuid, false, false);
}

该方法返回一个BluetoothServerSocket对象。该对象用于监听客户端的连接。

调用BluetoothServerSocket.accept(),可以阻塞当前线程直到超时或获取到一个客户端的连接。

注意:该方法可在获取到连接后再次调用。因此是允许多个客户端连接到一个服务端的。

/**
 * @param timeout 超时时间,-1表示为无限时
*/
public BluetoothSocket accept(int timeout) throws IOException {
    return mSocket.accept(timeout);
}

该方法返回一个BluetoothSocket对象。利用该对象,可以和客户端以IO流的形式进行数据通信。

和客户端通信

调用BluetoothSocket.getInputStream()BluetoothSocket.getOutputStream()即可获得输入流和输出流。后续的通信操作与常见的IO通信一样。

关闭

关闭客户端的连接可以调用BluetoochSocket.close()

关闭服务端可以调用BluetoothServerSocket.close()。关闭服务端之后,已连接的客户端依然可以继续通信。只是后续无法再让新的客户端连接上来。要中断与客户端的连接,请调用上一行提到的方法。

客户端

扫描服务端

扫描经典蓝牙设备,需要注册广播BluetoothDevice.ACTION_FOUND。然后再调用BluetoothAdapter.startDiscovery()

注意:6.0之后需要定位权限才能扫描到设备。

此时,发现的设备会通过广播的形式接收到。

// 广播接收器
private val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (BluetoothDevice.ACTION_FOUND == intent?.action) {
            // 发现设备
            val device =
              intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
        }
    }
}

连接服务端

连接服务端需要调用BluetoothDevice.createInsecureRfcommSocketToServiceRecord()。方法接收一个参数uuiduuid需要和服务端开启时使用的uuid一致。

public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid) throws IOException {
    // 检查蓝牙是否可用
    if (!isBluetoothEnabled()) {
        Log.e(TAG, "Bluetooth is not enabled");
        throw new IOException();
    }
    return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, false, false, this, -1,new ParcelUuid(uuid));
}

和服务端通信

和上面服务端介绍的一样。通过inputstreamoutputstream进行IO流通信。

关闭

调用BluetoothSocket.close()可关闭与服务端的连接。

2. 低功耗蓝牙BLE

基础知识

Gatt

BluetoothGatt来表示BLE设备。

关于Gatt需要详细了解的话可以看看这篇文章:《蓝牙BLE: GATT Profile 简介(GATT 与 GAP)》

可以简单的将其理解为BLE设备即可。所有BLE设备之间的通信都需要通过BluetoothGatt来实现。

Service

翻译为服务,可以简单理解为表示该BLE设备支持哪些功能。通常服务和特性(Characteristics)是成对出现的。需要详细了解的话,可以看看这篇文章:《BLE4.0教程二 蓝牙协议之服务与特征值分析》

Characteristics

翻译为特性、特征。该对象需要依托于Service存在。通常,进行数据交互的时候实际操作的对象就是Characteristics。该对象有以下几个比较重要的参数:

  1. 属性proprty,描述该特性的功能。常见取值有:PROPERTY_READ(0x02)、PROPERTY_WRITE_NO_RESPONSE(0x04)、PROPERTY_WRITE(0x08)、PROPERTY_NOTIFY(0x10)、PROPERTY_INDICATE(0x20)
  2. 权限permission,和属性基本是一一对应的。常见取值有:PERMISSION_READ(0x01)、PERMISSION_WRITE(0x10)
  3. 值value,特性的数据内容。格式为byte数组。

服务端

开启服务端

开启服务端需要调用BluetoothManager.openGattServer()

/** 
 * 开启BLE服务端
 * @param context 上下文
 * @param callback 回调参数,关键!
 * @return BluetoothGattServer instance
 */
public BluetoothGattServer openGattServer(Context context,
        BluetoothGattServerCallback callback) {

    return (openGattServer(context, callback, BluetoothDevice.TRANSPORT_AUTO));
}

该方法接收两个参数,最关键参数为callback。该参数用于接收客户端发来的请求以及客户端与服务端的连接状态。

下面为几个比较常用的callback的方法。

BluetoothGattServerCallback() {
    /**
     * 接收到客户端发来的读特性请求,该请求需要服务端回复客户端,否则客户端会断开连接
     * 
     * @param device 客户端设备
     * @param requestId 标识请求的id
     * @param offset 数据偏移量
     * @param characteristic 客户端希望读取的特性
    */
    override fun onCharacteristicReadRequest(
        device: BluetoothDevice?,
        requestId: Int,
        offset: Int,
        characteristic: BluetoothGattCharacteristic?
    ) {}

    /**
     * 接收到客户端发来的写特性请求
     *
     * @param device 客户端设备
     * @param requestId 标识请求的id
     * @param charateristic 客户端希望写的特性
     * @param preparedWrite 是否可以延时写入
     * @param responseNeeded 该请求是否需要服务端回复
     * @param offset 数据偏移量
     * @param value 要写入的数据
     */
    override fun onCharacteristicWriteRequest(
        device: BluetoothDevice?,
        requestId: Int,
        characteristic: BluetoothGattCharacteristic?,
        preparedWrite: Boolean,
        responseNeeded: Boolean,
        offset: Int,
        value: ByteArray?
    ) {}

    /**
     * 客户端设备连接状态改变
     * @param device 客户端设备
     * @param status 当前状态
     * @param newState 只有两种情况,已连接BluetoothProfile#STATE_CONNECTED;断开连接			 * BluetoothProfile#STATE_DISCONNECTED
     */
    override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) {}

    /**
     * 新增服务成功
     * @param status BluetoothGatt#GATT_SUCCESS表示成功,否则为失败
     * @param service 新增的服务
     */
    override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
        val isSuccess = status == 0
        listener?.onServiceAdded(service, isSuccess)
    }
}

开启广播

需要开启广播,客户端才能使用BLE的扫描方式扫描到设备。但是广播可以设置为不可连接,这样客户端即使扫描到设备,也不能连接到服务端。

调用BluetoothGattServer.getBluetoothLeAdvertiser().startAdvertising()。方法接收最多4个参数。

/**
 *
 * @param settings 广播设置
 * @param advertiseData 广播内容
 * @param scanResponse 扫描回复包,可不传
 * @param callback 回调函数,用于判断是否广播成功
 */
public void startAdvertising(AdvertiseSettings settings,
        AdvertiseData advertiseData, AdvertiseData scanResponse,
        final AdvertiseCallback callback) {}
广播设置
val advertiseSettings = AdvertiseSettings.Builder()
    .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) //广播模式: 低功耗,平衡,低延迟
    .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //发射功率级别: 极低,低,中,高
    .setConnectable(true) //能否连接,广播分为可连接广播和不可连接广播
    .build()
广播内容
val advertiseData = AdvertiseData.Builder()
    .setIncludeDeviceName(true) //包含蓝牙名称
    .setIncludeTxPowerLevel(true) //包含发射功率级别
    .addManufacturerData(1, byteArrayOf(66, 66)) //设备厂商数据,自定义
    .build()
扫描回复包
val scanResponse = AdvertiseData.Builder()
    .addManufacturerData(2, byteArrayOf(66, 66)) //设备厂商数据,自定义
    .addServiceUuid(ParcelUuid(你的服务uuid)) //服务UUID
    .addServiceData(new ParcelUuid(你的服务uuid), new byte[]{2}) //服务数据,自定义
    .build()

新增服务

调用BluetoothGattServer.addService()即可新增一个服务。实例化一个服务对象BluetoothGattService()需要一个UUID。服务可以没有特性,那么该服务无法提供数据传输的功能。

新增服务的结果会在BluetoothGattServerCallback.onServiceAdded()中接收。

新增特性

调用BluetoothGattService.addCharacteristic()即可。实例化一个特性对象BluetoothGattCharacteristic()需要传入三个参数。

/**
 * 特性构造函数
 * @param uuid UUID标识
 * @param properties 特性属性
 * @param permissions 特性权限
 */
public BluetoothGattCharacteristic(UUID uuid, int properties, int permissions) {
    initCharacteristic(null, uuid, 0, properties, permissions);
}

特性独立存在没有任何作用。需要将特性添加到一个服务中,并将该服务添加到BLE服务端才能生效。注意,如果是向已存在的服务追加特性的话,需要将服务从服务端中移除BluetoothGattServer.removeService(),然后再将追加特性后的服务添加到服务端BluetoothGattServer.addService()

回复客户端请求

当收到读请求,即BluetoothGattServerCallback.onCharacteristicReadRequest()时。需要及时回复请求,否则客户端将会断开连接。具体时间暂时未在源码找到。

调用BluetoothGattServer.sendResponse()回复请求。

/**
 * 回复客户端请求
 * @param device 客户端设备
 * @param requestId 请求的标识id,在接收请求的时候拿到
 * @param status 状态码
 * @param offset 数据偏移量
 * @param value 回复数据
 * @return 返回true表示回复已经发送成功(不代表客户端一定接收到)
 */
public boolean sendResponse(BluetoothDevice device, int requestId,
        int status, int offset, byte[] value) {
    if (VDBG) Log.d(TAG, "sendResponse() - device: " + device.getAddress());
    if (mService == null || mServerIf == 0) return false;

    try {
        mService.sendResponse(mServerIf, device.getAddress(), requestId,
                status, offset, value);
    } catch (RemoteException e) {
        Log.e(TAG, "", e);
        return false;
    }
    return true;
}

主动通知客户端

服务端改变特性的内容BluetoothGattCharacteristic.setValue(),然后调用BluetoothGattServer.notifyCharacteristicChanged()

/** 
 * 
 * @param device 客户端设备
 * @param characteristic 改变的特性
 * @param confirm true的话为indication,false为notification
 * @return true, if the notification has been triggered successfully
 * @throws IllegalArgumentException
 */
public boolean notifyCharacteristicChanged(BluetoothDevice device,
        BluetoothGattCharacteristic characteristic, boolean confirm) {}

这个方法需要注意的是第三个参数confirm。作用是客户端是否需要主动查询一次信息。为true的时候,客户端应当发起一次查询特性请求,然后才把特性的内容回复给客户端。为false的时候,则客户端可以直接读取特性中的内容,无需再发送请求。

但是经过我自己测试发现,似乎无论confirm传true还是false,客户端都可直接在在回调中接收到数据。emmm。

关闭服务端

调用BluetoothGattServer.close()即可。

客户端

扫描服务端

调用BluetoothAdapter.getBluetoothLeScanner().startScan()。该方法接收一个ScanCallback作为回调函数。

ScanCallback() {
    override fun onScanFailed(errorCode: Int) {
        Log.e(tag, "扫描BLE设备失败,错误码:$errorCode")
    }

    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        // 拿到正在广播的设备
        val device = result?.device
        // result.isConnectable用于判断该广播是否可以用来连接,API26以上
    }
}

连接服务端

调用BluetoothDevice.connectGatt()

/** 
 *
 * @param callback 回调
 * @param autoConnect 是否要自动连接,一般为false,不然的话连接速度大大降低
 */
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
        BluetoothGattCallback callback) {}

callback是关键参数。服务端发来的所有回复都在这个回调类中进行接收处理的。

以下为该类的几个常用回调函数:

BluetoothGattCallback() {
    /**
     * 读特性请求的回复
     * @param gatt 服务端对象
     * @param characteristic 请求读的特性
     * @param status 请求结果,为0表示成功
     */
    override fun onCharacteristicRead(
        gatt: BluetoothGatt?,
        characteristic: BluetoothGattCharacteristic?,
        status: Int
    ) {}

    /**
     * 写特性请求回复
     * @param gatt 服务端对象
     * @param characteristic 请求写的特性
     * @param status 请求结果,为0表示成功
     */
    override fun onCharacteristicWrite(
        gatt: BluetoothGatt?,
        characteristic: BluetoothGattCharacteristic?,
        status: Int
    ) {}

    /**
     * discoverService()这个方法的回调
     * @param gatt 服务端对象
     * @param status 操作结果,为0表示成功
     */
    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {}

    /**
     * 与服务端的连接状态发生变化
     * @param gatt 服务端对象
     * @param status 这个没啥用
     * @param newSate 当前连接状态。BluetoothProfile#STATE_DISCONNECTED或者				 * BluetoothProfile#STATE_CONNECTED
     */
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {}
    
    /**
     * 特性内容发生改变,由服务端进行通知,直接读取参数中的特性value即可
     * @param gatt 服务端对象
     * @param characteristic 内容发生改变的特性
     */
    override fun onCharacteristicChanged(
                gatt: BluetoothGatt?,
                characteristic: BluetoothGattCharacteristic?
     ) {}
}

关于autoConnect,如果设置为true的话,那么连接速度会大大的降低。但是可以自动连接到设备,这样可以不用自己去管理当连接突然中断后又要自己重新扫描去连接。

回调方法中的BluetoothGatt是与服务端交互的关键类。

查询服务

调用BluetoothGatt.discoverServices()

虽然还有个方法BlueGatt.getServices()但是请注意!!!getServices()需要在调用discoverServices()之后,在BluetoothGattCallback.onServicesDiscovered()中确定status为0,才能正确的获取最新最全的服务端支持的服务。

另外,如果服务端的服务是在客户端连接后才新增,那么客户端需要重新连接一次之后才能获取到最新的服务。否则是没办法看到的。

发送读特性请求

调用BluetoothGatt.readCharacteristic()

/**
 * @param characteristic 希望读取的特性
 * @return true表示请求发送成功
 */
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
     // 需要特性拥有属性PROPERTY_READ
     if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) return false;
     ....
 }

服务端会在收到请求后进行回复,如果服务端没有进行回复,那么一段时间(我也不知道是多久)后客户端就会断开和服务端的连接。

发送写特性请求

调用BluetoothGatt.writeCharacteristic()

/** 
 * @param characteristic 希望写入的特性,该特性的value就是希望写入的内容
 * @return true表示请求发送成功
 */
public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic) {
     // 要求特性有PROPERTY_WRITE属性或PROPERTY_WRITE_NO_RESPONSE属性
    if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0 && (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) {
        return false;
    }
 }

如果特性的属性是PROPERTY_WRITE,那么同样需要服务端在收到请求后进行回复,否则一段时间后客户端就会断开与服务端的连接。

注册特性通知

调用BluetoothGatt.setCharacteristicNotification(),并在第二个参数传入true。根据源码,发现即使特性没有PROPERTY_NOTIFY属性,依然可以注册,并且服务端的通知也能传过来。so,PROPERTY_NOTIFY有啥用?

关闭客户端

调用BluetoothGatt.close()

结束语

可以看到,原生的蓝牙使用起来还是比较繁琐的,在日常开发中肯定要进行一些基础的封装。我也根据我的使用需求,进行了一下简单的封装,可以看看下面这篇文章。
Android】蓝牙快速开发工具包-入门级

你可能感兴趣的:(Android)