当前位置: 首页 > 工具软件 > ble > 使用案例 >

深入理解蓝牙BLE

欧阳智志
2023-12-01

Android蓝牙开发分为经典蓝牙和低功耗蓝牙

经典蓝牙:蓝牙3.0版本以下的蓝牙。
低功耗蓝牙:蓝牙4.0(及以上版本)

两者的区别很明显,虽然都叫做蓝牙,但已经算是两个东西了;流程的话都类似,协议不同

发现设备->配对/绑定设备->建立连接->数据通信

经典蓝牙和低功耗蓝牙除了配对/绑定这个环节是一样的之外,其它三个环节都是不同的。

1. 发现设备

  • 经典蓝牙:经典蓝牙设备发现其它经典蓝牙设备的方式是调用BluetoothAdapter的startDiscovery()方法。
    api上说的比较模糊,大致是说只能够发现经典蓝牙设备。
    然而实验发现 BluetoothAdapter.startDiscovery是可以同时发现经典蓝牙和ble的

  • 低功耗蓝牙:低功耗蓝牙中则有一个主设备(Central)和从设备(Peripheral,也叫外围设备)的概念。主设备作为发现方,调用发现设备的方法,通过BluetoothAdapter的startLeScan()方法实现。从设备则作为被发现方,发出广播,以供发现。同样,这个startLeScan()方法也仅能够发现低功耗蓝牙从设备。

总结:BluetoothAdapter.startDiscovery在大多数手机上是可以同时发现经典蓝牙和Ble的,但是startDiscovery的回调无法返回Ble的广播,所以无法通过广播识别设备,且startDiscovery扫描Ble的效率比StartLeScan低很多。所以在实际应用中,还是StartDiscovery和StartLeScan分开扫,前者扫传统蓝牙,后者扫低功耗蓝牙。

注意:当两种蓝牙设备被某设备(包括当前的设备)配对/绑定后,可能不会再被扫描到。

2. 配对/绑定

有很多小伙伴都不太理解配对和绑定究竟有什么区别,或者它们根本就是同一个东西。好吧,严格说配对和绑定是有区别的,也就是不是指的同一件事情。但是这两者的区别比较模糊,也不好解释。目前JACK的机器人的理解是,配对是建立两者的对应关系,而绑定则把这层关系保存固定下来并进行了强化,暂时这么理解着吧。

不管是经典蓝牙还是低功耗蓝牙,绑定方法都是通用的,可以调用相同的绑定方法。

3. 建立连接

*在建立连接的方式上,两者就千差万别了。

在蓝牙设备中,存在着物理地址,我们也叫作蓝牙的MAC地址,这个地址是唯一的,就像咱们网络上的IP地址。同时还存在着一个叫做UUID的东西,可以把它理解为是IP地址中的端口号。正如知道了IP地址和端口号,就知道了怎么链接到目标网络服务器位置,知道了蓝牙设备的MAC地址和UUID也就能够确定到具体是哪一台蓝牙设备了,这两者合起来就是蓝牙的唯一身份标识。

经典蓝牙:经典蓝牙建立连接的方式实际上就是Socket的连接的建立。只不过这里不是直接用Socket,而是BluetoothSocket。获取BluetoothSocket的方式也很简单,利用搜索找到的BluetoothDevice,调用其方法createRfcommSocketToServiceRecord(UUID)。最后,使用获取到的BluetoothDevice调用其方法connect()就建立了经典蓝牙设备之间的连接通道。

低功耗蓝牙:低功耗蓝牙则用了一种看起来比较怪异的方式建立连接。

  • Generic Attribute Profile (GATT)
    通过BLE连接,读写属性类小数据的Profile通用规范。现在所有的BLE应用Profile都是基于GATT的。
    Attribute Protocol (ATT)
    GATT是基于ATT Protocol的。ATT针对BLE设备做了专门的优化,具体就是在传输过程中使用尽量少的数据。每个属性都有一个唯一的UUID,属性将以characteristics and services的形式传输。
    Characteristic
    Characteristic可以理解为一个数据类型,它包括一个value和0至多个对次value的描述(Descriptor)。
    Descriptor
    对Characteristic的描述,例如范围、计量单位等。
    Service
    Characteristic的集合。例如一个service叫做“Heart Rate Monitor”,它可能包含多个Characteristics,其中可能包含一个叫做“heart rate measurement”的Characteristic。

    举个栗子,现在需要使用一个手机作为主设备去连接一个作为从设备的智能手表,那么,此时这个作为主设备的手机连接过程中实际是一个客户端(Client),而作为从设备的手表在此过程中则是服务端(Server)。这里的主设备和从设备,客户端和服务端一定要区分清楚。

想要和一台BLE从设备建立连接,如果使用手机作为测试平台,其硬件条件是,蓝牙得至少是低功耗蓝牙版本,然后安卓系统的话,至少得是Android 4.3以上系统才行,因为Google在Android 4.3以上才做了BLE主设备的支持,如果想将智能手机作为BLE从设备,则必须在Android 5.0以上才行。

具体建立GATT连接的顺序则是,首先通过BluetoothAdapter的getRemoteDevice(address)方法获取大相应BLE从设备的BluetoothDevice,其中的address为目标蓝牙设备MAC地址。然后通过此BluetoothDevice的connectGatt(this, false, mGattCallback)方法获取设备连接。

此时的连接,只能够进行监听,也就是获取到当前BLE从设备广播出来的数据。

4. 数据通信

经典蓝牙:当建立连接后,就可以直接使用BluetoothSocket的getOutputStream()方法获取输出流写入需要发送的数据。读取发送回来的数据,则是调用BluetoothSocket的getInputStream()方法获取输入流读取。这点和Java中的Socket通信几乎是一模一样。
低功耗蓝牙:想要实现主设备对从设备的数据发送,则需要直接读取获取到的从设备的Characteristic,而Characteristic又是Service下面的一层,所以操作顺序是:

  • 通过BLE从设备相应的Service_UUID获取对应的BluetoothGattService,获取方法是:使用BluetoothDevice的connectGatt(this, false, mGattCallback)方法返回的BluetoothGatt对象,调用BluetoothGatt的方法getService(Service_UUID)获取相应的BluetoothGattService;
  • 调用BluetoothGattService和对应的Characteristic的写入UUID获取相应的BluetoothGattCharacteristic,获取方法是:调用BluetoothGattService的getCharacteristic(Characteristic_UUID)方法获得;
  • 设置需要发送的命令值,调用BluetoothGattCharacteristic的方法setValue(value)进行设置,其中value一般为byte[];
  • 最后,使用BluetoothGatt的写入方法writeCharacteristic(TxChar)完成命令发送。

可以看到,想要实现BLE的数据通信,步骤相当繁琐,这里只是做一个简单的概念理解,如果想要获取到BLE从设备的返回值,还需要设置Notification,然后调用BluetoothGatt的readCharacteristic(characteristic)方法进行数据的读取;

一般来说一个BLE蓝牙设备,直接调用connect和disconnect方法就实现连接和断开了


    /**
     * Disconnects an established connection, or cancels a connection attempt
     * currently in progress.
     *
     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     */
    public void disconnect() {
        if (DBG) Log.d(TAG, "cancelOpen() - device: " + mDevice.getAddress());
        if (mService == null || mClientIf == 0) return;

        try {
            mService.clientDisconnect(mClientIf, mDevice.getAddress());
        } catch (RemoteException e) {
            Log.e(TAG, "", e);
        }
    }

    /**
     * Connect back to remote device.
     *
     * <p>This method is used to re-connect to a remote device after the
     * connection has been dropped. If the device is not in range, the
     * re-connection will be triggered once the device is back in range.
     *
     * @return true, if the connection attempt was initiated successfully
     */
    public boolean connect() {
        try {
            mService.clientConnect(mClientIf, mDevice.getAddress(), false, mTransport,
                    mOpportunistic, mPhy); // autoConnect is inverse of "isDirect"
            return true;
        } catch (RemoteException e) {
            Log.e(TAG, "", e);
            return false;
        }
    }

通过看BluetoothGatt的源码发现,里面有个close方法,调用了unregisterApp,比较好奇就研究了下;


    /**
     * Close this Bluetooth GATT client.
     *
     * Application should call this method as early as possible after it is done with
     * this GATT client.
     */
    public void close() {
        if (DBG) Log.d(TAG, "close()");

        unregisterApp();
        mConnState = CONN_STATE_CLOSED;
        mAuthRetryState = AUTH_RETRY_STATE_IDLE;
    }

    /**
     * Unregister the current application and callbacks.
     */
    private void unregisterApp() {
        if (DBG) Log.d(TAG, "unregisterApp() - mClientIf=" + mClientIf);
        if (mService == null || mClientIf == 0) return;

        try {
            mCallback = null;
            mService.unregisterClient(mClientIf);
            mClientIf = 0;
        } catch (RemoteException e) {
            Log.e(TAG, "", e);
        }
    }


追了下源码发现是反注册,置为空闲状态;这是什么场景下使用的呢?
作为程序猿就喜欢发现问题,产生疑问,研究问题,解决TA,抱着怀疑的心来干TA吧,莱茨狗;
刚好手头有几个型号的设备,就写了一个Demo,扫描、连接操作不再累赘,连接成功之后我就反复连接断开,连接断开,哦哦哦,问题出现了,为啥明明通过MacAddress调用了disconnect方法,通过广播接收者还能收到广播数据呢?习惯性的摸起来脑袋瓜子,一休一休哟…

  public void connect(String address) {
        connectAddress = address;
        if (mBluetoothAdapter == null)
            return;

        BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
        if (device == null)
            return;
        //去掉这段return代码
//        if (address.equals(device.getAddress()) && mBluetoothGatt != null) {
//            Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
//            mBluetoothGatt.connect();
//            return;
//        }
        //gatt 直接反注册
        if (mBluetoothGatt!=null){
            mBluetoothGatt.close();
        }
        //重新初始化gatt
        mBluetoothGatt = device.connectGatt(this, false, mGattCallbacks);
    }

发现这里有个BluetoothGatt会重用,在界面销毁时才会取消服务调用close,根据我现在的业务是,多个设备同时使用,我是不可能反注册这个服务的,无论是重用gatt还是多次创建,BluetoothGattCallback回调数据不知道是哪个gatt的,会不会是这里的问题呢?那就改改看看,手动调用close方法,保证gatt和device永远只有一个且一一对应;

附上:蓝牙BLE调试Demo源码

 类似资料: