美文网首页
Android IOT 蓝牙(三)

Android IOT 蓝牙(三)

作者: 古早味蛋糕 | 来源:发表于2023-03-07 19:34 被阅读0次

一、低功耗蓝牙发展史
传统蓝牙虽然历史悠久,但是它的缺陷也很明显,包括但不限于下列几点:
(1)需要两部设备配对之后才能继续连接,而且连接速度也慢。
(2)连接之后就一直保持传输链路,很消耗电能。
(3)数据传输的有效距离不到10米,导致使用场景受限。
为解决传统蓝牙的上述痛点,蓝牙技术联盟制定了低功耗蓝牙技术,并于2012年纳入蓝牙4.0规范。低功耗蓝牙又称蓝牙低能耗(Bluetooth Low Energy,BLE),与之相对应的,蓝牙4.0之前的蓝牙技术被称作经典蓝牙,也称传统蓝牙。因为BLE采取非常快速的连接方式,所以平时处于“非连接”状态,此时链路两端仅是知晓对方,只有在必要时才开启链路,完成传输后会尽快关闭链路。BLE技术与之前版本的蓝牙标准相比,主要有三个方面的改进:更省电、连接速度更快、传输距离更远。更详尽的蓝牙技术发展历程如图【蓝牙技术发展历程】所示


蓝牙技术发展历程.png

低功耗蓝牙不同于传统蓝牙,它规定所有BLE设备遵循统一的通用属性规范(Generic Attribute Profile,GATT),基于该规范制定了BLE通信的基本守则。为了理清BLE设备之间的交互过程,有必要解释一下相关术语:

  • BLE从机,又称服务端,它接受GATT指令,并根据指令调整自身行为,例如蓝牙灯泡、蓝牙锁、蓝牙小车等。

  • BLE主机,又称客户端,它向服务端发送GATT指令,令其遵照指令行事,例如操控蓝牙小车的手机等。

  • 特征值(characteristic),BLE通过参数来传输数据,服务端定好一个参数,然后客户端对该参数进行读、写、通知等操作,这种参数被称作特征值。

  • 服务(service),一个特征值往往不够用,比如这个特征值专用于灯光亮度,那个特征值专用于灯光颜色,存在多个特征值的话可能还会对它们分类,分好的种类配一个UUID(Universally Unique Identifier,通用唯一识别码)就被称作服务。一个设备可拥有多个服务,每个服务也可包含多个特征值,每个特征值又存在多种属性(properties),例如长度(size)、权限(permission)、值(value)、描述符(descriptor)等。把上述术语关联起来,形成GATT规范的内容框架,如图【GATT规范的内容框架】所示:


    GATT规范的内容框架.png

    了解了BLE技术的基本概念,再来动手尝试App的BLE编程。由于BLE集成于蓝牙4.0,因此AndroidManifest.xml同样要声明蓝牙的相关权限,此外还要通过uses-feature声明仅在支持BLE的设备上运行,权限声明的配置如下所示:

    <!-- 蓝牙 -->
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission
      android:name="android.permission.BLUETOOTH_PRIVILEGED"
      tools:ignore="ProtectedPermissions" />
    <!-- 仅在支持BLE(即蓝牙4.0)的设备上运行 -->
    <uses-feature
      android:name="android.hardware.bluetooth_le"
      android:required="true" />
    <!-- 如果Android6.0 蓝牙搜索不到设备,需要补充下面两个权限 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    

现在绝大多数智能手机都支持BLE,这些手机既能充当BLE从机(服务端),又可以充当BLE主机(客户端)。把手机当作客户端使用的话,就得主动扫描周围有哪些服务端设备。当然,在扫描开始前要先初始化蓝牙适配器,初始化代码示例如下BleScanActivity完整代码

private BluetoothAdapter mBluetoothAdapter; // 声明一个蓝牙适配器对象
private BluetoothDevice mRemoteDevice; // 声明一个蓝牙设备对象
private BluetoothGatt mBluetoothGatt; // 声明一个蓝牙GATT客户端对象
// 初始化蓝牙适配器
private void initBluetooth() {
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        Toast.makeText(this, "当前设备不支持低功耗蓝牙", Toast.LENGTH_SHORT).show();
        finish(); // 关闭当前页面
    }
    // 获取蓝牙管理器,并从中得到蓝牙适配器
    BluetoothManager bm = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBluetoothAdapter = bm.getAdapter(); // 获取蓝牙适配器
}

接着调用蓝牙适配器的getBluetoothLeScanner方法,获得BluetoothLeScanner扫描器对象。这个扫描器主要有startScan和stopScan两个方法,其中startScan方法表示开始扫描BLE设备,调用它的示例代码如下:

// 创建一个开启BLE扫描的任务
@SuppressLint("MissingPermission")
private Runnable mScanStart = new Runnable() {
    @Override
    public void run() {
        if (!isScaning && BluetoothUtil.getBlueToothStatus()) {
            isScaning = true;
            // 获取BLE设备扫描器
            BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
            scanner.startScan(mScanCallback); // 开始扫描BLE设备
            tv_discovery.setText("正在搜索低功耗蓝牙设备");
        } else {
            mHandler.postDelayed(this, 2000);
        }
    }
};

stopScan方法表示停止扫描BLE设备,调用它的示例代码如下:

// 创建一个停止BLE扫描的任务
@SuppressLint("MissingPermission")
private Runnable mScanStop = () -> {
    isScaning = false;
    // 获取BLE设备扫描器
    BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
    scanner.stopScan(mScanCallback); // 停止扫描BLE设备
    tv_discovery.setText("停止搜索低功耗蓝牙设备");
};

无论startScan方法还是stopScan方法,它们都需要传入回调对象的参数,如同监听器那样,一旦收到扫描结果,就触发回调对象的指定方法。下面是扫描回调对象的示例代码:

// 创建一个扫描回调对象
@SuppressLint("MissingPermission")
private ScanCallback mScanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);
        if (TextUtils.isEmpty(result.getDevice().getName())) {
            return;
        }
        Log.d(TAG, "callbackType="+callbackType+", result="+result.toString());
        // 下面把找到的蓝牙设备添加到设备映射和设备列表
        BlueDevice device = new BlueDevice(result.getDevice().getName(), result.getDevice().getAddress(), 0);
        mDeviceMap.put(device.address, device);
        mDeviceList.clear();
        mDeviceList.addAll(mDeviceMap.values());
        runOnUiThread(() -> mListAdapter.notifyDataSetChanged());
    }

    @Override
    public void onBatchScanResults(List<ScanResult> results) {
        super.onBatchScanResults(results);
    }

    @Override
    public void onScanFailed(int errorCode) {
        super.onScanFailed(errorCode);
    }
};

以上的扫描回调代码表明,收到扫描结果会触发onScanResult方法,其参数为ScanResult类型,调用它的getDevice方法即可获得BLE服务端的设备对象。也可调用蓝牙适配器的getRemoteDevice方法,根据对方的MAC地址得到它的设备对象。之后再调用设备对象的connectGatt方法,连接GATT服务器并获得客户端的GATT对象,示例代码如下:

        // 根据设备地址获得远端的蓝牙设备对象
        mRemoteDevice = mBluetoothAdapter.getRemoteDevice(item.address);
        Log.d(TAG, "onItemClick address="+mRemoteDevice.getAddress()+", name="+mRemoteDevice.getName());
        // 连接GATT服务器
        mBluetoothGatt = mRemoteDevice.connectGatt(this, false, mGattCallback);

注意,connectGatt方法的第三个输入参数为BluetoothGattCallback类型,表示这里要传入事先定义的GATT回调对象。下面是定义一个GATT回调对象的示例代码:

    // 发现BLE服务端的服务列表及其特征值时回调
    @Override
    public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        Log.d(TAG, "onServicesDiscovered status"+status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            // 获取GATT服务器提供的服务列表
            List<BluetoothGattService> gattServiceList= mBluetoothGatt.getServices();
            for (BluetoothGattService gattService : gattServiceList) {
                List<BluetoothGattCharacteristic> charaList = gattService.getCharacteristics();
                for (BluetoothGattCharacteristic chara : charaList) {
                    int charaProp = chara.getProperties(); // 获取该特征的属性
                    if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
                        read_UUID_chara = chara.getUuid();
                        read_UUID_service = gattService.getUuid();
                        Log.d(TAG, "read_chara=" + read_UUID_chara + ", read_service=" + read_UUID_service);
                    }
                    if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
                        write_UUID_chara = chara.getUuid();
                        write_UUID_service = gattService.getUuid();
                        Log.d(TAG, "write_chara=" + write_UUID_chara + ", write_service=" + write_UUID_service);
                    }
                    if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) {
                        write_UUID_chara = chara.getUuid();
                        write_UUID_service = gattService.getUuid();
                        Log.d(TAG, "no_response write_chara=" + write_UUID_chara + ", write_service=" + write_UUID_service);
                    }
                    if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
                        notify_UUID_chara = chara.getUuid();
                        notify_UUID_service = gattService.getUuid();
                        Log.d(TAG, "notify_chara=" + notify_UUID_chara + ", notify_service=" + notify_UUID_service);
                    }
                    if ((charaProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
                        indicate_UUID_chara = chara.getUuid();
                        indicate_UUID_service = gattService.getUuid();
                        Log.d(TAG, "indicate_chara=" + indicate_UUID_chara + ", indicate_service=" + indicate_UUID_service);
                    }
                }
            }
        } else {
            Log.d(TAG, "onServicesDiscovered fail-->" + status);
        }
    }

    @Override
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic chara, int status) {
        super.onCharacteristicRead(gatt, chara, status);
        Log.d(TAG, "onCharacteristicRead");
        if (status == BluetoothGatt.GATT_SUCCESS) {}
    }

    // 收到BLE服务端的数据变更时回调
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic chara) {
        super.onCharacteristicChanged(gatt, chara);
        String message = new String(chara.getValue()); // 把服务端返回的数据转成字符串
        Log.d(TAG, "onCharacteristicChanged "+message);
    }

    // 收到BLE服务端的数据写入时回调
    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic chara, int status) {
        super.onCharacteristicWrite(gatt, chara, status);
        Log.d(TAG, "onCharacteristicWrite");
        if (status == BluetoothGatt.GATT_SUCCESS) {}
    }

    @Override
    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        super.onDescriptorWrite(gatt, descriptor, status);
        Log.d(TAG, "onDescriptorWrite");
        if (status == BluetoothGatt.GATT_SUCCESS) {}
    }

    @Override
    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
        super.onReadRemoteRssi(gatt, rssi, status);
        Log.d(TAG, "onReadRemoteRssi");
        if (status == BluetoothGatt.GATT_SUCCESS) {}
    }

    @Override
    public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        super.onDescriptorRead(gatt, descriptor, status);
        Log.d(TAG, "onDescriptorRead");
        if ((status == BluetoothGatt.GATT_SUCCESS)) {}
    }
};

虽然BluetoothGattCallback接口定义了许多方法,但是简单应用只需下列4个方法:
● onConnectionStateChange:BLE连接的状态发生变化时回调。如果连接成功,就调用GATT对象的discoverServices方法查找BLE服务;如果连接失败,就调用GATT对象的close方法关闭连接。
● onServicesDiscovered:发现BLE服务端的服务列表及其特征值时回调。如果执行成功,就调用GATT对象的getServices方法,获取GATT规范的服务列表,并遍历每个服务的特征值,分析服务端都提供了哪些服务与客户端交互。注意,必须事先在onConnectionStateChange中调用GATT对象的discoverServices方法,接着才会触发这里的onServicesDiscovered方法。如下【特征值类型的取值说明表】


特征值类型的取值说明.png

● onCharacteristicChanged:收到BLE服务端的数据变更时回调。该方法会收到服务端送来的消息。
● onCharacteristicWrite:收到BLE服务端的数据写入时回调。如果执行成功,就表示服务端已经收到客户端发给它的消息。
若想在界面上显示扫描发现的设备,则可重写onConnectionStateChange方法,在连接成功的过程中将新设备添加至列表。补充界面列表的刷新代码之后,先打开手机的蓝牙功能,再运行并测试该App,可观察到扫描周围的BLE设备得到的结果,如下图所示


某商场的BLE扫描结果.png
二、发送BLE广播
上面手机可以扫描发现周围的BLE设备,其实手机自身也能变成BLE设备让别人发现。不过仅仅开启蓝牙功能尚不足以使其变身,还得让手机向外发送BLE广播。在发送BLE广播之前要初始化蓝牙适配器,示例代码如下:
private BluetoothAdapter mBluetoothAdapter; // 声明一个蓝牙适配器对象
private BluetoothDevice mRemoteDevice; // 声明一个蓝牙设备对象
private BluetoothGatt mBluetoothGatt; // 声明一个蓝牙GATT客户端对象
// 初始化蓝牙适配器
private void initBluetooth() {
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        Toast.makeText(this, "当前设备不支持低功耗蓝牙", Toast.LENGTH_SHORT).show();
        finish(); // 关闭当前页面
    }
    // 获取蓝牙管理器,并从中得到蓝牙适配器
    BluetoothManager bm = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBluetoothAdapter = bm.getAdapter(); // 获取蓝牙适配器
}

接着调用蓝牙适配器的getBluetoothLeAdvertiser方法,获得BluetoothLeAdvertiser广播器对象。这个广播器主要有startAdvertising和stopAdvertising两个方法。其中,startAdvertising方法表示开始发送BLE广播,调用它的示例代码如下:BleAdvertiseActivity完整代码

// 开始低功耗蓝牙广播
@SuppressLint("MissingPermission")
private void startAdvertise(String ble_name) {
    // 设置广播参数
    AdvertiseSettings settings = new AdvertiseSettings.Builder()
            .setConnectable(true) // 是否允许连接
            .setTimeout(0) // 设置超时时间
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
            .build();
    // 设置广播内容
    AdvertiseData advertiseData = new AdvertiseData.Builder()
            .setIncludeDeviceName(true) // 是否把设备名称也广播出去
            .setIncludeTxPowerLevel(true) // 是否把功率电平也广播出去
            .build();
    mBluetoothAdapter.setName(ble_name); // 设置BLE服务端的名称
    // 获取BLE广播器
    BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
    // BLE服务端开始广播,好让别人发现自己
    advertiser.startAdvertising(settings, advertiseData, mAdvertiseCallback);
}

stopAdvertising方法表示停止发送BLE广播,调用它的示例代码如下:BleAdvertiseActivity完整代码

// 停止低功耗蓝牙广播
@SuppressLint("MissingPermission")
private void stopAdvertise() {
    if (mBluetoothAdapter != null) {
        // 获取BLE广播器
        BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
        if (advertiser != null) {
            advertiser.stopAdvertising(mAdvertiseCallback); // 停止低功耗蓝牙广播
        }
    }
}

无论是startAdvertising方法还是stopAdvertising方法,它们都需要传入回调对象的参数,如同监听器一样,一旦成功发送广播,就触发回调对象的指定方法。下面是定义一个广播回调对象的示例代码:BleAdvertiseActivity完整代码

// 创建一个低功耗蓝牙广播回调对象
private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
    @Override
    public void onStartSuccess(AdvertiseSettings settings) {
        Log.d(TAG, "低功耗蓝牙广播成功:"+settings.toString());
        addService(); // 添加读写服务UUID,特征值等
        String desc = String.format("BLE服务端“%s”正在对外广播", et_name.getText().toString());
        tv_hint.setText(desc);
    }

    @Override
    public void onStartFailure(int errorCode) {
        Log.d(TAG, "低功耗蓝牙广播失败,错误代码为"+errorCode);
        tv_hint.setText("低功耗蓝牙广播失败,错误代码为"+errorCode);
    }
};

以上的广播回调代码表明,成功发送广播会触发onStartSuccess方法,在该方法中要给BLE服务端添加服务及其特征值,并开启GATT服务器等待客户端连接。添加服务的示例代码如下:BleAdvertiseActivity完整代码

// 添加读写服务UUID,特征值等
@SuppressLint("MissingPermission")
private void addService() {
    BluetoothGattService gattService = new BluetoothGattService(
            BleConstant.UUID_SERVER, BluetoothGattService.SERVICE_TYPE_PRIMARY);
    // 只读的特征值
    BluetoothGattCharacteristic charaRead = new BluetoothGattCharacteristic(BleConstant.UUID_CHAR_READ,
            BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
            BluetoothGattCharacteristic.PERMISSION_READ);
    // 只写的特征值
    BluetoothGattCharacteristic charaWrite = new BluetoothGattCharacteristic(BleConstant.UUID_CHAR_WRITE,
            BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
            BluetoothGattCharacteristic.PERMISSION_WRITE);
    gattService.addCharacteristic(charaRead); // 将特征值添加到服务里面
    gattService.addCharacteristic(charaWrite); // 将特征值添加到服务里面
    // 开启GATT服务器等待客户端连接
    mGattServer = mBluetoothManager.openGattServer(this, mGattCallback);
    mGattServer.addService(gattService); // 向GATT服务器添加指定服务
}

注意,openGattServer方法的第二个输入参数为BluetoothGattServerCallback类型,表示这里要传入事先定义的GATT服务器回调对象。下面是一个GATT服务器回调对象的示例代码:

// 创建一个GATT服务器回调对象
@SuppressLint("MissingPermission")
private BluetoothGattServerCallback mGattCallback = new BluetoothGattServerCallback() {
    // BLE连接的状态发生变化时回调
    @Override
    public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
        super.onConnectionStateChange(device, status, newState);
        Log.d(TAG, "onConnectionStateChange device=" + device.toString() + " status=" + status + " newState=" + newState);
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            runOnUiThread(() -> {
                String desc = String.format("%s\n已连接BLE客户端,对方名称为%s,MAC地址为%s",
                        tv_hint.getText().toString(), device.getName(), device.getAddress());
                tv_hint.setText(desc);
            });
        }
    }

    // 收到BLE客户端写入请求时回调
    @Override
    public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic chara, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
        super.onCharacteristicWriteRequest(device, requestId, chara, preparedWrite, responseNeeded, offset, value);
        String message = new String(value); // 把客户端发来的数据转成字符串
        Log.d(TAG, "收到了客户端发过来的数据 " + message);
    }
};

BluetoothGattServerCallback接口定义了许多方法,不过简单应用只需下列两个方法:
● onConnectionStateChange:BLE连接的状态发生变化时回调。此时如果已经连接,就从输入参数获取客户端的设备对象,并处理后续的连接逻辑。
● onCharacteristicWriteRequest:收到BLE客户端写入请求时回调。该方法会收到客户端发来的消息。
分别在两部手机上安装测试App,一部手机充当BLE服务端,另一部手机充当BLE客户端。在服务端手机上点击开始广播按钮,此时正在广播的服务端界面如下图【服务端手机正在对外广播】所示;接着让客户端手机进入扫描页面,可以找到正在广播的BLE服务端,如图【客户端扫描发现BLE服务端】所示。


服务端手机正在对外广播.png
客户端扫描发现BLE服务端.png

三、通过主从BLE实现聊天应用
上面讲到手机通过向外发送BLE广播成为BLE服务端,进而被另一部手机在扫描BLE设备时发现,那么两部手机之间又该如何通信呢?这里牵涉到GATT服务端与GATT客户端的交互,首当其冲的是BluetoothGattServer和BluetoothGatt两个类,其中前者充当GATT服务端的角色,后者充当GATT客户端的角色。为了夯实BLE技术基础,有必要熟悉一下这两个GATT工具的用法。
首先看BluetoothGattServer,它的对象是怎么得到的呢?原来调用蓝牙管理器对象的openGattServer方法会开启GATT服务器并返回BluetoothGattServer类型的服务端对象,之后可通过服务端对象进行GATT相关操作。BluetoothGattServer的常用方法如下:
● addService:向GATT服务器添加指定服务。
● sendResponse:向GATT客户端发送应答,告诉它成功收到了要写入的数据。
● notifyCharacteristicChanged:向GATT客户端发送本地特征值已更新的通知。
● close:关闭GATT服务器。其次看BluetoothGatt,之前在上面提到,调用蓝牙设备对象的connectGatt方法会连接GATT服务器并返回BluetoothGatt类型的客户端对象,之后可通过客户端对象与服务端进行通信。BluetoothGatt的常用方法如下:
● discoverServices:开始查找GATT服务器提供的服务,查找成功会触发onServicesDiscovered方法。
● getServices:获取GATT服务器提供的服务列表。
● writeCharacteristic:往GATT服务器写入特征值。
● setCharacteristicNotification:开启或关闭特征值的通知(第二个输入参数为true表示开启)。开启之后才能收到服务器的特征值更新通知。
● disconnect:断开GATT连接。
● close:关闭GATT客户端。
GATT服务端与客户端的通信流程,主要包括三个方面:建立GATT连接、客户端向服务端发消息、服务端向客户端发消息。
1、建立GATT连接
首先需要打开服务器,然后客户端才能连上服务器。在GATT规范中,所有特征值都被封装在服务中,有了服务之后才允许读写特征值,故而GATT服务端的addService方法在先,GATT客户端的discoverServices方法在后。GATT服务端与客户端的连接过程如图【ATT服务端与客户端的连接过程】所示。


GATT服务端与客户端的连接过程.png

2、客户端向服务端发消息
GATT客户端调用writeCharacteristic方法,会往GATT服务器写入特征值。服务器收到请求后,需调用GATT服务端的sendResponse方法向GATT客户端发送应答,告诉它成功收到了要写入的数据。此时客户端触发onCharacteristicWrite方法,表示收到了服务端的写入应答。客户端写入与服务端应答的交互过程如图【客户端向服务端发消息的过程】所示。


客户端向服务端发消息的过程.png

3、服务端向客户端发消息
服务端向客户端发消息,客户端理应无条件收到才是,但是GATT规范偏偏规定了通知开关,只有客户端开启了通知才会收到服务端的消息;一旦客户端关闭了通知,那么服务端干什么它都漠不关心了。GATT客户端开启了通知后,GATT服务端调用notifyCharacteristicChanged方法向客户端发送特征值变更通知,然后触发客户端的onCharacteristicChanged方法,客户端再在该方法中处理收到的消息。服务端写特征值并通知客户端的交互过程如图【服务端向客户端发消息的过程】所示。

服务端向客户端发消息的过程.png
搞清楚GATT服务端与客户端的通信流程,接着书写BLE通信代码便好办多了,无非是按部就班照流程套模板而已。然而BLE的读写操作可不简单,因为GATT规范划分了好几种特征值,有只读特征值、可写特征值、通知特征值、指示特征值等,其中只读特征值只允许客户端读、不允许客户端写,可写特征值既允许客户端读也允许客户端写,不同种类的特征值分别适用不同的业务场景。
另外,GATT规范要求每次传输的消息大小不能超过20字节,若待传输的消息长度超过了20字节,就得切片后分次传输。比如GATT服务端向客户端发消息,需要先按照20字节将消息切片再将切片后的消息列表依次发出去。GATT服务端发送消息的示例代码如下:BleServerActivity完整代码
// 发送聊天消息
@SuppressLint("MissingPermission")
private void sendMessage() {
    String message = et_input.getText().toString();
    if (TextUtils.isEmpty(message)) {
        Toast.makeText(this, "请先输入聊天消息", Toast.LENGTH_SHORT).show();
        return;
    }
    et_input.setText("");
    ViewUtil.hideOneInputMethod(this, et_input); // 隐藏软键盘
    List<String> msgList = ChatUtil.splitString(message, 20); // 按照20字节切片
    for (String msg : msgList) {
        mReadChara.setValue(msg); // 设置读特征值
        // 发送本地特征值已更新的通知
        mGattServer.notifyCharacteristicChanged(mRemoteDevice, mReadChara, false);
    }
    appendChatMsg(message, true); // 往聊天窗口添加聊天消息
}

至于接收消息,不存在消息过长的问题,无论收到什么消息都原样接收处理。下面是GATT服务端接收客户端消息的示例代码:BleServerActivity完整代码

    // 收到BLE客户端写入请求时回调
    @Override
    public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic chara, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
        super.onCharacteristicWriteRequest(device, requestId, chara, preparedWrite, responseNeeded, offset, value);
        String message = new String(value); // 把客户端发来的数据转成字符串
        Log.d(TAG, "收到了客户端发过来的数据 " + message);
        // 向GATT客户端发送应答,告诉它成功收到了要写入的数据
        mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, chara.getValue());
        runOnUiThread(() -> appendChatMsg(message, false)); // 往聊天窗口添加聊天消息
    }

GATT客户端不仅要将消息切片,还不能一股脑地连续发送切片消息。因为服务端接收消息需要时间,如果一下子涌来多条消息,那么前面的消息会被后面的消息覆盖,导致服务端只能处理最后一条消息。正确的做法是,每次发送切片消息之前先检查上次的消息是否被服务端成功接收。只有服务端已经收到上一条消息,客户端才能发送下一条消息,否则就得继续等待直至服务端确认收到上次消息。按照以上逻辑编写GATT客户端的消息发送代码,示例如下:BleClientActivity完整代码

private boolean isLastSuccess = true; // 上一条消息是否发送成功
// 发送聊天消息
private void sendMessage() {
    String message = et_input.getText().toString();
    if (TextUtils.isEmpty(message)) {
        Toast.makeText(this, "请先输入聊天消息", Toast.LENGTH_SHORT).show();
        return;
    }
    et_input.setText("");
    ViewUtil.hideOneInputMethod(this, et_input); // 隐藏软键盘
    new MessageThread(message).start(); // 启动消息发送线程
    appendChatMsg(message, true); // 往聊天窗口添加聊天消息
}

// 定义一个消息发送线程
@SuppressLint("MissingPermission")
private class MessageThread extends Thread {
    private List<String> msgList; // 消息列表
    public MessageThread(String message) {
        msgList = ChatUtil.splitString(message, 20);
    }

    @Override
    public void run() {
        // 拿到写的特征值
        BluetoothGattCharacteristic chara = mBluetoothGatt.getService(BleConstant.UUID_SERVER)
                .getCharacteristic(BleConstant.UUID_CHAR_WRITE);
        for (int i=0; i<msgList.size(); i++) {
            if (isLastSuccess) { // 需要等到上一条回调成功之后,才能发送下一条消息
                isLastSuccess = false;
                Log.d(TAG, "writeCharacteristic "+msgList.get(i));
                chara.setValue(msgList.get(i)); // 设置写特征值
                mBluetoothGatt.writeCharacteristic(chara); // 往GATT服务器写入特征值
            } else {
                i--;
            }
            try {
                sleep(300); // 休眠300毫秒,等待上一条的回调通知
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端上次发送的消息成功接收与否要在onCharacteristicWrite方法中校验,该方法判断如果是成功状态,则表示服务端的确收到了客户端消息,此时再更改成功与否的标志位。GATT客户端的写入回调方法的示例代码如下:BleClientActivity完整代码

    // 收到BLE服务端的数据变更时回调
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic chara) {
        super.onCharacteristicChanged(gatt, chara);
        String message = new String(chara.getValue()); // 把服务端返回的数据转成字符串
        Log.d(TAG, "onCharacteristicChanged "+message);
        runOnUiThread(() ->appendChatMsg(message, false)); // 往聊天窗口添加聊天消息
    }

接收消息不存在前述的繁文缛节,直接在onCharacteristicChanged方法中处理收到的消息即可。下面是GAT客户端接收服务端消息的示例代码:

    // 收到BLE服务端的数据写入时回调
    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic chara, int status) {
        super.onCharacteristicWrite(gatt, chara, status);
        Log.d(TAG, "onCharacteristicWrite status="+status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            isLastSuccess = true;
        } else {
            Log.d(TAG, "write fail->" + status);
        }
    }

相关文章

网友评论

      本文标题:Android IOT 蓝牙(三)

      本文链接:https://www.haomeiwen.com/subject/jwgildtx.html