蓝牙低功耗BLE篇——BluetoothGatt案例详解

话说我个人是做Android开发的,以前没做过关于蓝牙方面的开发。最近公司在做一个关于蓝牙4.0低功耗(BLE)的产品,前期的要求是写一个APP在手机或者平板上,要求能接收到BLE设备发出的数据然后显示在界面上。

相信看到这篇文章的人,或多或少都了解了BLE的特性和优点,我这里就不赘述了

当时我查阅Google开发者文档有关于BLE的介绍时,它提到Google托管了一个关于BLE的Sample在GitHub上,因此我以这个叫做BluetoothLeGatt的Sample作为案例来进行对BLE的讲述。项目地址:BluetoothLeGattt

首先,明确哪一个Activity作为启动的活动,通过清单文件我们很容易看出是DeviceScanActivity。这个Activity主要目的是搜寻BLE设备,并通过列表条目(ListView)的形式来展现出来。

从onCreate方法开始,前面代码一系列都是判断你手机或者平板设备是否支持蓝牙,以及获得BluetoothAdapter这个类的实例(注意:这个实例挺重要的)。执行完onCreate方法之后,进入onResume()方法,其关键方法在于scanLeDevice(true);这个方法是自定义方法,该方法里面实现了BLE设备的搜索,看下面代码:

	private void scanLeDevice(final boolean enable) {
		if (enable) {
	    //异步方法,表示延迟一定的时间之后执行其内部代码,
	    //在该程序中,表示在10秒之后,停止手机或者平板扫描BLE设备的动作
		    mHandler.postDelayed(new Runnable() {
		        @Override
		        public void run() {
		            mScanning = false;
	                mBluetoothAdapter.stopLeScan(mLeScanCallback);
	                invalidateOptionsMenu();
		            }
	        }, SCAN_PERIOD);
			mScanning = true;
	        mBluetoothAdapter.startLeScan(mLeScanCallback);
	     } else {
		     mScanning = false;
	         mBluetoothAdapter.stopLeScan(mLeScanCallback);
	     }
	     invalidateOptionsMenu();
	}

真正执行的扫描BLE设备的操作的是蓝牙适配器实例mBluethAdapter.startLeScan(mLeSacnCallback),扫描到的设备都在这个方法的参数mLeSacnCallback这个回调里面进行处理,让我们来看一下:

	// Device scan callback.
    private BluetoothAdapter.LeScanCallback mLeScanCallback =
            new BluetoothAdapter.LeScanCallback() {
 
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mLeDeviceListAdapter.addDevice(device);
                    Log.i(TAG,"添加一个设备");
                    mLeDeviceListAdapter.notifyDataSetChanged();
                }
            });
        }
    };

这个回调中,将扫描到的设备,都添加到一个集合中去,然后以ListView的条目形式显示出来(Sample中将这个过程进行了封装,反正大概意思就这样,就像程序还有ActionBar中的选项的处理一样,不用在意这些细节,如果刻意的话,反而会增加你对这个案例的理解的难度)。
好啦,到这里,程序的设备搜索操作就完成了。设备信息也通过条目(包含名字和设备的物理地址)的形式显示下界面上了。点击条目,通过意图传递设备名称和物理地址跳转到下一个Activity中:

@Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        final BluetoothDevice device = mLeDeviceListAdapter.getDevice(position);
        if (device == null) return;
        final Intent intent = new Intent(this, DeviceControlActivity.class);
        intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_NAME, device.getName());
        intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_ADDRESS, device.getAddress());
        if (mScanning) {
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
            mScanning = false;
        }
        startActivity(intent);

跳转的同时,顺便停止扫描。。。。好吧,下一个DeviceControlActivityBluetoothLeService两个才是这个Sample的核心内容,接着看。首先,在DeviceControlActivity中找到onCreate()生命周期方法:

	@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.gatt_services_characteristics);
 
        final Intent intent = getIntent();
        mDeviceName = intent.getStringExtra(EXTRAS_DEVICE_NAME);
        mDeviceAddress = intent.getStringExtra(EXTRAS_DEVICE_ADDRESS);
 
        // Sets up UI references.
        ((TextView) findViewById(R.id.device_address)).setText(mDeviceAddress);
        mGattServicesList = (ExpandableListView) findViewById(R.id.gatt_services_list);
        mGattServicesList.setOnChildClickListener(servicesListClickListner);
        mConnectionState = (TextView) findViewById(R.id.connection_state);
        mDataField = (TextView) findViewById(R.id.data_value);
 
        getActionBar().setTitle(mDeviceName);
        getActionBar().setDisplayHomeAsUpEnabled(true);
        Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
        //将Activity和Services绑定,BIND_ATUO_CREATE表示活动和服务绑定后自动创建服务,这里会使得
        //Services的onCreate()方法得到执行,但不是执行onStartCommand()方法
        bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
    }

onCreate方法一旦启动,先把所传递过来的设备名称和设备的物理地址获取。进而看到通过意图来绑定服务(四大组件之一),BluetoothLeService是一个服务,后面许多操作将会在该服务执行。当然,还是一步步来,bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE)(顺便看下注释),mServiceConnection是关键。

	private final ServiceConnection mServiceConnection = new ServiceConnection() {
 
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder service) {
            mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService();
            if (!mBluetoothLeService.initialize()) {
                Log.e(TAG, "Unable to initialize Bluetooth");
                finish();
            }
            // Automatically connects to the device upon successful start-up initialization.
            mBluetoothLeService.connect(mDeviceAddress);
        }
 
        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            mBluetoothLeService = null;
        }
    };

ServiceConnection的实例里面重写了两个方法:onServiceConnected()onServiceDisconnect()。分别表示连接服务成功和连接服务失败。在成功连接的方法里面:获取了BluetoothLeService服务的实例,进而初始化,再而就是关键的*connect(mDeviceAddress)方法,将上面获取到的设备物理地址作为参数传递过去。然后再进入该BluetoothLeService服务类中的connect()*方法中来围观一下,揭开面纱:

	public boolean connect(final String address) {
        if (mBluetoothAdapter == null || address == null) {
            Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
            return false;
        }
 
        // Previously connected device.  Try to reconnect.
        if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
                && mBluetoothGatt != null) {
            Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
            if (mBluetoothGatt.connect()) {
                mConnectionState = STATE_CONNECTING;
                return true;
            } else {
                return false;
            }
        }
 
        final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
        if (device == null) {
            Log.w(TAG, "Device not found.  Unable to connect.");
            return false;
        }
        // true表示自动连接,false表示立刻连接,好吧,应该明白哪个是你需要的了吧。
        mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
        Log.d(TAG, "Trying to create a new connection.");
        mBluetoothDeviceAddress = address;
        mConnectionState = STATE_CONNECTING;
        return true;
    }

戳进来,前面又是一堆严谨的蓝牙是否可用判断。。。看到BluetoothDevice,咳咳,肉戏终于来了!通过传递过来的设备物理地址address来获取期盼已久的BluetoothDevice的实例。。。。。。什么,你问我mBluetoothAdapter在该服务里面什么时候进行的定义和赋值?!好吧,找了又找,原来是在DeviceControlActivity类中mServiceConnection实例中上面初始化方法*initialize()*里面进行了定义和赋值!!!请看:

	public boolean initialize() {
        // For API level 18 and above, get a reference to BluetoothAdapter through
        // BluetoothManager.
        if (mBluetoothManager == null) {
            mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
            if (mBluetoothManager == null) {
                Log.e(TAG, "Unable to initialize BluetoothManager.");
                return false;
            }
        }
 
        mBluetoothAdapter = mBluetoothManager.getAdapter();//I am here!
        if (mBluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
            return false;
        }
 
        return true;
    }

是不是很坑!!对!就是那么坑!刚才说到获取了BluetoothDevice这个关键类的实例device,然后mBluetoothGatt = device.connectGatt(this, false, mGattCallback),终于建立了手机与设备的联系。那么真真的关键就来了,就在mGattCallback这个回调实例中。许多和蓝牙设备的交互就是从这实例中完成的。戳进这个实例来看:

	//实现应用程序所关心的GATT事件的接口回调方法,比如:连接的状态,服务的发现等
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                    broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                // Attempts to discover services after successful connection.
                //尝试发现服务在成功连接之后
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());
 
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }
 
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }
 
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            }
        }
 
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {
            broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
        }
    };

该回调实例重写了四个方法。

  • 第一个重写的方法onContectionStateChange方法,根据其参数newState,判断手机是否和蓝牙设备连接是否成功,然后执行发送广播的操作(封装在*broadcastUpdate()*方法中)。执行该方法的情景:当手机试图与蓝牙设备进行连接的时候的执行该方法。

  • 第二个重写的方法onServiceDiscovered,表示发现设备中的服务(ps:设备中的服务和特征还有uuid会在一篇内容中介绍,现在可以理解设备的服务是一辆客车,特征就是乘客,uuid是车牌。而乘客中有我们所需要的数据),如果成功发现了服务,就执行发送广播的操作。执行该方法的情景:当手机发现蓝牙设备的服务时候。

  • 第三个重写的方法onCharacteristicRead方法,执行的是特征(characteristic)读操作,然后执行发送广播的操作,这个操作和上述的操作不同,等下戳进去观察一下。执行该方法的情景:调用mBluetoothGatt.readCharacteristic(characteristic) 方法的时候执行。

  • 第四个重写的方法onCharacteristicChange方法,是当蓝牙设备内部的特征(characteristic)发生变化是执行的操作方法。方法中执行重载的发送广播的方法。我们结合第三重写方法来观察一下这个方法。

private void broadcastUpdate(final String action,final BluetoothGattCharacteristic characteristic) {
        final Intent intent = new Intent(action);
        //这是给心率测量规范的特殊处理,数据解析是根据配置文件进行规范的
        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
            int flag = characteristic.getProperties();
            int format = -1;
            if ((flag & 0x01) != 0) {
                format = BluetoothGattCharacteristic.FORMAT_UINT16;
                Log.d(TAG, "Heart rate format UINT16.");
            } else {
                format = BluetoothGattCharacteristic.FORMAT_UINT8;
                Log.d(TAG, "Heart rate format UINT8.");
            }
            final int heartRate = characteristic.getIntValue(format, 1);
            Log.d(TAG, String.format("Received heart rate: %d", heartRate));
            intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
        } else {
            // For all other profiles, writes the data formatted in HEX.
            //如果不是心率测量的规范,将数据转化为16进制的数据
            final byte[] data = characteristic.getValue();
            if (data != null && data.length > 0) {
                final StringBuilder stringBuilder = new StringBuilder(data.length);
                for(byte byteChar : data)
                    stringBuilder.append(String.format("%02X ", byteChar));
		            intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
            }
        }
        sendBroadcast(intent);
    }

这个封装的方法,大概就是从特征(characteristic)中获取蓝牙传递过来的数据。但是一个蓝牙设备中有许多个服务,一个服务中又包含了一个或者多个特征,特征下面才是需要的获得的数据,那么该如何做出区分呢?这是就要用到UUID这个概念,但是详细介绍需要许多的篇幅,因此在这就认为它是一个车牌号,用来寻找指定特征(characteristic)的标识。看方法代码:判断特征的UUID是否心率测量的UUID是否相等(心率测量有指定的profile,本文并不关心,若想了解可以查阅部分资料和蓝牙开发者门户网站),若相等根据指定的心率解析规范(profile)进行解析。若不相等,当做一般的数据来进行解析(本文侧重一般解析)。一般解析中,很明显就是byte[] data就是蓝牙设备中传递过来的数据——一个字节数组,然后将数组转换成字符串。通过Intent将数据携带,然后发送广播。

既然广播已经发出,那肯定有接收广播的一方,回到DeviceControlActivity中,在那里已经动态注册了广播接收者(四大组件之一)了:

private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
                mConnected = true;
                updateConnectionState(R.string.connected);
                invalidateOptionsMenu();
            } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
                mConnected = false;
                updateConnectionState(R.string.disconnected);
                invalidateOptionsMenu();
                clearUI();
            } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
                // Show all the supported services and characteristics on the user interface.
                //显示所有的被支持的服务和属性在用户的界面上
                displayGattServices(mBluetoothLeService.getSupportedGattServices());
            } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
                displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
            }
        }
    };
 registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());

根据已经过滤好的动作,进行不同的判断,来分别做不同操作。

  • BluetoothLeService.ACTION_GATT_CONNECTED是手机和蓝牙设备连接上的动作,表示手机和蓝牙设备已经处在连接的状态,然后将已连接状态显示在界面上。

  • BluetoothLeService.ACTION_GATT_DISCONNECTED是手机和蓝牙设备没有连接的动作,表示手机和蓝牙设备处在无连接的状态,然后将未连接的状态显示在界面上。

  • BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED是发现蓝牙设备服务(service)的动作,*mBluetoothLeService.getSupportedGattServices()方法是得到一个服务的集合,然后将这个蓝牙设备所有的服务的集合作为参数传递,然后displayGattServices()*这个方法,是将这个蓝牙设备所有的服务和服务下面的特性用ExpendListView(二级结构)显示在界面上,而点击服务条目就展开该服务所在的所有特性,点击特征目录则显示数据在上方。(这个封装的方法略有复杂,若看不懂也不必太过纠结)。

  • BluetoothLeService.ACTION_DATA_AVAILABLE是读取特征(characteristic)和特征变化时的动作,并且将从特征得到的数据通过Intent返回,然后显示在界面上。

接下来,就是关于2级条目的点击事件,里面包含了触发mGattCallback中读取属性的状态和一旦属性特征变化的onCharacteristicChange方法的触发事件:

	private final ExpandableListView.OnChildClickListener servicesListClickListner =
            new ExpandableListView.OnChildClickListener() {
                @Override
                public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
                                            int childPosition, long id) {
                    if (mGattCharacteristics != null) {
                        //获得的是BluetoothGattCharacteristic
                        final BluetoothGattCharacteristic characteristic =
                                mGattCharacteristics.get(groupPosition).get(childPosition);
                        final int charaProp = characteristic.getProperties();
                        if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
                            // If there is an active notification on a characteristic, clear
                            // it first so it doesn't update the data field on the user interface.
                            //假如有一个活跃的notification在characteristic中,首先先清理它,因
                            //为它不会在用户的界面上更新数据字段
                            if (mNotifyCharacteristic != null) {
                                mBluetoothLeService.setCharacteristicNotification(
                                        mNotifyCharacteristic, false);
                                mNotifyCharacteristic = null;
                            }
                            mBluetoothLeService.readCharacteristic(characteristic);
                        }
                        if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
                            mNotifyCharacteristic = characteristic;
                            mBluetoothLeService.setCharacteristicNotification(
                                    characteristic, true);
                        }
                        return true;
                    }
                    return false;
                }
    };

方法里面,首先是获得被点击的是的特征(Characteristic)是哪一个,进而进行判断(看注释),在于*setCharacteristicNotification()方法,它用于该特征的数据变化时进行同步的更新和变化,以及readCharacteristic(characteristic)方法,读取属性的方法。先进入readCharacteristic(characteristic)*方法进行观察:

	public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.readCharacteristic(characteristic);
    }

方法很明显,mBluetoothGatt.readCharacteristic(characteristic) 就是读取属性的方法,该方法调用之后,就会触发mGattCallback回调中的*onCharacteristicRead()方法,然后在onCharacteristicRead()方法中发送广播。 而setCharacteristicNOtification()*方法主要用于当该特征在蓝牙设备上的数据进行了变化的时候,程序中能够通知更新。看代码:

	public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
                                              boolean enabled) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
 
        // This is specific to Heart Rate Measurement.
        //这是特定的,是心率的测量的处理
        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                    UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBluetoothGatt.writeDescriptor(descriptor);
        }
    }

方法里面直接一句mBluetoothGatt.setCharacteristicNotification(characteristic, enabled) 就完成了通知更新?!NO!后面才是重点,首先先过滤出你想要实时更新的特征(Characteristic),然后设置描述(Descriptor),然后writeDescriptor(descriptor),这样该特征才会通知更新。所以,有些人没注意这些细节,连通蓝牙设备之后运行完该程序之后,就会奇怪为什么我点击的特性的值为什么没有通知同步更新,而在该特征下蓝牙设备上发送的数据的确是不同的,问题在于:该程序的只设定了心率的那个UUID下特性的通知更新,就是刚才所述的代码,若要指定某个特征进行通知更新就要重新设置描述,以及然后writeDescriptor(descriptor)。哦,还有一点,CLIENT_CHARACTERISTIC_CONFIG是特性通用的uuid,一般情况下是固定不变的。

好了,说到这里,差不多整个Sample就差不多讲完了,还有关闭连接,关闭服务的一些细节可以参考官方给出的文档。

在该文的末尾,来捋一遍程序代码的思路。

  1. 扫描蓝牙设备,然后通过条目形式展示搜索到的设备。点击条目,将设备名称和物理地址传递到下一个活动当中。

  2. 绑定服务,在绑定服务的参数中ServiceConnection的实例中的方法中,获取自定义服务的实例,根据服务实例调用*connect()*方法,将设备的物理地址传递过去。

  3. connect()方法目的是根据设备的物理地址,连接到蓝牙设备上GATT服务,该过程中,有个GattCallBack回调,该回调完成了连接设备、发现服务、读取特征,数据变化通知更新等操作,然后通过广播的形式将操作结果返回到活动中去。

  4. 自定广播接收者,设置过滤,将广播的内容的获取,然后进行相应的处理。

若文中知识点出现错误,还请前辈大神批评指正。

你可能感兴趣的:(蓝牙低功耗开发)