关于蓝牙4.0低功耗(BLE)Android开发的一点浅谈(1)

来源:互联网 发布:un服务贸易数据库 编辑:程序博客网 时间:2024/06/05 18:59

话说我个人是做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. 自定广播接收者,设置过滤,将广播的内容的获取,然后进行相应的处理。


最后的最后,我会在下一篇的讲述该文没有讲述清楚的一些蓝牙概念,以及蓝牙设备在Android4.3以上的开发中,所涉及开发有关的蓝牙部分知识的阐述以及和我学习的部分见解。若文中知识点出现错误,还请前辈大神批评指正。

4 0