Android低功耗蓝牙(BLE)随笔(二)

Android中的实现

1. 扫描广播包

  • 首先获取 BluetoothManagerBluetoothAdapter
  private BluetoothAdapter mBluetoothAdapter;
  BluetoothManager bluetoothManager = (BluetoothManager)   context.getSystemService(Context.BLUETOOTH_SERVICE);
  mBluetoothAdapter= bluetoothManager.getAdapter();
  • 开始扫描
    /**
     * 开始扫描,扫描结果在 BluetoothAdapter.LeScanCallback 中返回
     * @param deviceName 扫描的设备名
     */
    public void startScan(String deviceName) {
        if (mBluetoothAdapter == null) {
            // error
            return;
        } 
        BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
        this.mScanning = true;
        List<ScanFilter> filters = new ArrayList<>();
        ScanFilter filter = new ScanFilter.Builder()
                .setDeviceName(deviceName)
                .build();
        filters.add(filter);
        scanner.startScan(filters, new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(), mScanCallback);
    }

这里的扫描设置了过滤的参数,只返回设备名为传入参数的广播。如果要扫描特定的设备,也可以根据设备的Mac地址过滤。或者也可以用不过滤方式扫描,过滤逻辑可以在扫描结果中处理。

    private ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            // 广播的信息,可以在result中获取
            Log.e(TAG, "onScanResult: name: " + result.getDevice().getName() +
                                ", address: " + result.getDevice().getAddress() +
                                ", rssi: " + result.getRssi() + ", scanRecord: " + result.getScanRecord());
        }
    };

开启蓝牙扫描会增加手机功耗,Android官方也不建议一直进行扫描,因此无论有没有扫描到目标设备,都应该设置一个超时时间,避免长时间扫描。停止扫描指令如下:

    public void stopScan() {
        BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
        scanner.stopScan(mScanCallback);
    }

2. 建立蓝牙连接

如果发现需要进行蓝牙连接的设备,就可以发起建立蓝牙连接请求:

    /**
     * 建立蓝牙连接
     * @param address 设备的Mac地址
     * @result 连接请求是否成功
     */
    public boolean connect(Context context, String address) {
        if (mBluetoothAdapter == null || address == null) {
            Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
            error();
            return false;
        }
        Log.i(TAG, "connecting ");
        // Previously connected device.  Try to reconnect.
        if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
                && mBluetoothGatt != null) {
            Log.i(TAG, "尝试使用已有的GATT连接");
            if (mBluetoothGatt.connect()) {
                return true;
            } else {
                return false;
            }
        }

        final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
        if (device == null) {
            Log.i(TAG, "Device not found.  Unable to connect.");
            return false;
        }
        // 连接的核心代码
        mBluetoothGatt = device.connectGatt(context, false, mGattCallback);
        Log.d(TAG, "Trying to create a new connection.");
        return true;
    }

连接是否成功可以在回调函数中获取:

private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                mConnectionState = STATE_CONNECTED;
                Log.i(TAG, "Connected to GATT server.");
                // 连接成功
                ...
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                mConnectionState = STATE_DISCONNECTED;
                // 断开连接
                ...
            } else {
                // 其他错误情况
            }
        }
    };

onConnectionStateChange方法在连接状态改变后被调用,status是之前状态,newState是改变后的状态。类BluetoothGattCallback 除了得到连接状态的变化,还可以得到其他信息。稍后会逐步介绍。

3. 发现服务

如果蓝牙连接成功,就可以通过GATT协议进行通讯。前面介绍过,蓝牙的信息是通过特征值(characteristics)和服务(services)的传输。因此首先得获取设备的服务:

    public void discoverServices() {
        mBluetoothGatt.discoverServices();
    }

发现服务的结果在BluetoothGattCallback的另一个方法中返回:

    private BleDeviceService  mBleDeviceService ;
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                //获取服务成功
                //可用gatt.getServices()获取Service,并用BleDeviceService缓存起来,供访问使用。
                mBleDeviceService = new BleDeviceService(gatt.getServices());
            } else {
                //获取服务失败
            }
        }
    };

其中BleDeviceService 用List缓存了相关的Service,并提供了用UUID查询的方法:

public class BleDeviceService {
    private final List<BluetoothGattService> bluetoothGattServices;

    public BleDeviceService(List<BluetoothGattService> bluetoothGattServices) {
        this.bluetoothGattServices = bluetoothGattServices;
    }

    /**
     * 获取全部 BluetoothGattService
     */
    public List<BluetoothGattService> getBluetoothGattServices() {
        return bluetoothGattServices;
    }

    /**
     * 获取特定 BluetoothGattService
     * @param serviceUuid UUID service的标识
     * @return BluetoothGattService 查找不到返回null
     */
    public BluetoothGattService getService(@NonNull final UUID serviceUuid){
        for (BluetoothGattService bluetoothGattService : bluetoothGattServices) {
            if (bluetoothGattService.getUuid().equals(serviceUuid)){
                return bluetoothGattService;
            }
        }
        return null;
    }

    /**
     * 获取特定特征值
     * @param characteristicUuid 特征值UUID
     * @return BluetoothGattCharacteristic 特征值,查找不到返回null
     */
    public BluetoothGattCharacteristic getCharacteristic(@NonNull UUID serviceUuid,@NonNull UUID characteristicUuid) {
        BluetoothGattService bluetoothGattService = getService(serviceUuid);
        if (bluetoothGattService != null){
            BluetoothGattCharacteristic characteristic = bluetoothGattService.getCharacteristic(characteristicUuid);
            if (characteristic != null){
                return characteristic;
            }
        }
        return null;
    }
}

4. 读写特征值

有了Service的信息,就可以进行读写特征值了,读特征值是获取设备的信息,写特征值是,发送信息给设备。
读特征值:

/**
     * 读特征值
     *
     * @param serviceUUID        服务 UUID
     * @param characteristicUUID 特征值 UUID
     */
    public void readCharacteristic(String serviceUUID, String characteristicUUID) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        boolean isError = false;
        if (mBleDeviceService != null) {
            BluetoothGattCharacteristic characteristic = mBleDeviceService.getCharacteristic(UUID.fromString(serviceUUID),
                    UUID.fromString(characteristicUUID));
            if (characteristic != null) {
                int permission = characteristic.getProperties();
                if ((permission & BleConstants.AT_BLE_PERMISSION_CHAR_READ)
                        == BleConstants.AT_BLE_PERMISSION_CHAR_READ) {
                    Log.i(TAG, "reading characteristic");
                    mBluetoothGatt.readCharacteristic(characteristic);
                } else {
                    Log.i(TAG, "read permission denied");
                    isError = true;
                }
            } else {
                Log.i(TAG, "characteristic is null");
                isError = true;
            }
        } else {
            Log.i(TAG, "mBleDeviceService is null");
            isError = true;
        }
        if (isError) {
            // 处理错误
        }
    }

写特征值:

    private void write(String serviceUUID, String characteristicUUID, byte[] data) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            //错误
            return;
        }
        if (mBleDeviceService != null) {
            BluetoothGattCharacteristic characteristic = mBleDeviceService.getCharacteristic(UUID.fromString(serviceUUID),
                    UUID.fromString(characteristicUUID));
            if (characteristic != null) {
                int permission = (byte) characteristic.getProperties();
                if ((permission & BleConstants.AT_BLE_PERMISSION_CHAR_WRITE)
                        == BleConstants.AT_BLE_PERMISSION_CHAR_WRITE ||
                        (permission & BleConstants.AT_BLE_PERMISSION_CHAR_SIGNED_WRITE)
                                == BleConstants.AT_BLE_PERMISSION_CHAR_SIGNED_WRITE ||
                        (permission & BleConstants.AT_BLE_PERMISSION_CHAR_WRITE_WITHOUT_RESPONSE)
                                == BleConstants.AT_BLE_PERMISSION_CHAR_WRITE_WITHOUT_RESPONSE ||
                        (permission & BleConstants.AT_BLE_PERMISSION_CHAR_RELIABLE_WRITE)
                                == BleConstants.AT_BLE_PERMISSION_CHAR_RELIABLE_WRITE) {
                 //可以设置特征值的类型,默认为有应答。
                //characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);   
                    characteristic.setValue(data);
                    mBluetoothGatt.writeCharacteristic(characteristic);
                    Log.i(TAG, "writing characteristic done");
                } else {
                    Log.i(TAG, "writing permission denied");
                    //error
                }
            } else {
                Log.i(TAG, "null characteristic");
               //error
            }
        }
    }

这里注释的这一行(characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);)需要说明一下,如果不设置,默认的写特征值类型是有应答的。有无应答体现在底层通信上,不会影响到写特征值的回调函数的调用。目前发现区别是:对某些蓝牙芯片,无应答的通讯速率会略快,而且对调大的最大MTU(Maximum Transmission Unit,传输单元),也会减少出错的几率。
读写是否成功的结果同样也是在回调函数中获得:

private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.i(TAG, "read characteristic success");
                // 通过characteristic.getValue()获取信息
                ...
            } else {
                Log.i(TAG, "read characteristic fail " + status);
            }

        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.i(TAG, "characteristic write success ");
                ...

            } else {
                Log.i(TAG, "characteristic write fail " + status);
                ...

            }
        }
    };

5. 使能和接收通知

读特征值的方式只能是手机去读取设备的信息,而不能设备主动发送信息过来。如果想要实现设备主动发送信息给手机,必须手机端先使能一个特征值,之后设备就可以通过这个特征值发送信息。
使能的方法:

/**
     * 使能通知
     *
     * @param serviceUUID        服务UUID
     * @param characteristicUUID 特征值UUID
     * @param notificationFlag   是否开启
     */
    public void toggleNotification(String serviceUUID, String characteristicUUID, boolean notificationFlag) {
        if ( mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
             // error
            return;
        }
        if (mBleDeviceService != null) {
            BluetoothGattCharacteristic characteristic = mBleDeviceService.getCharacteristic(UUID.fromString(serviceUUID),
                    UUID.fromString(characteristicUUID));
            if (characteristic != null) {
                int permission = (byte) characteristic.getProperties();
                if ((permission & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
                    UUID CCC = UUID.fromString(BleConstants.CONFIG_DESCRIPTOR);
                    mBluetoothGatt.setCharacteristicNotification(characteristic, notificationFlag); //Enabled locally
                    BluetoothGattDescriptor config = characteristic.getDescriptor(CCC);
                    config.setValue(notificationFlag ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE :
                            BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
                    mBluetoothGatt.writeDescriptor(config); //Enabled remotely
                } else {
                    Log.i(TAG, "characteristic has no notification property");
                    // error
                }
            } else {
                Log.i(TAG, "null characteristic");
                 // error
            }
        }
    }

获取使能是否成功的结果以及接收设备发送来的信息的回调方法:

private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            Log.i(TAG, "onCharacteristicChanged: " + Arrays.toString(characteristic.getValue()) + " " +
                    characteristic.getUuid().toString());
            // 可以用characteristic.getValue()读取信息。
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.i(TAG, "Notification/indication  enabled successfully");
                ...
            } else {
                Log.i(TAG, "Notification/indication enabled failed");
                // error
            }
        }

    };

代码中用的是Notification,还有另一个方法是关于Indication的(将toggleNotification方法内的BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE替换为BluetoothGattDescriptor.ENABLE_INDICATION_VALUE),二者的区别也是前者类似TCP后者类似UDP。

5. 断开连接

最后,就是通讯结束后断开连接的方法:

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

推荐阅读更多精彩内容