使用帮助 | 联系电话:400-880-0256 0769-23037585 21686281
字体大小:返回
您当前的位置:首页 > 公告中心 > IT科技 > 低功耗蓝牙(3)

低功耗蓝牙(3)

作者:admin 发表于:2014-08-14 点击:1924  保护视力色:

在本系列之前的文章中,我们了解了 Bluetooth LE(低功耗蓝牙,后文简称为BLE)的一些背景并且构建了一个简单的Activity/Service模式的蓝牙低功耗框架。在今天的文章里,我们将更深入的探讨BLE的技术细节,并且实现BLE下的“设备发现”功能。

发现设备简单的说,是在蓝牙可见范围内搜索可用设备的过程。为了避免一开始就因为缺乏权限而无法实现搜索,首先我们需要在AndroidManifest中添加必要的权限。我们所需要添加的权限有android.permission.BLUETOOTH以及android.permission.BLUETOOTH_ADMIN。其中第一个权限是 Android使用蓝牙所必要的权限,而第二个则是一些蓝牙附加功能的权限,比如本次讨论的发现设备功能。

在我们开始一头钻入代码之前,有必要解释一下本文中的BleService是以状态机的形式运作的。BleService可以在不同的状态下执行不同的任务,所以我们从第一个状态——SCANNING开始切入。BleService会在收到一条名为MSG_START_SCAN的消息后进入这个状态。

private static class IncomingHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        BleService service = mService.get();
        if (service != null) {
            switch (msg.what) {
                .
                .
                .
                case MSG_START_SCAN:
                    service.startScan();
                    Log.d(TAG, "Start Scan");
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}

然后是开始搜索的startScan()函数代码:

public class BleService extends Service implements
    BluetoothAdapter.LeScanCallback {
    private final Map mDevices = 
        new HashMap();
 
    public enum State {
        UNKNOWN,
        IDLE,
        SCANNING,
        BLUETOOTH_OFF,
        CONNECTING,
        CONNECTED,
        DISCONNECTING
    }
    private BluetoothAdapter mBluetooth = null;
    private State mState = State.UNKNOWN;
    .
    .
    .
    private void startScan() {
        mDevices.clear();
        setState(State.SCANNING);
        if (mBluetooth == null) {
            BluetoothManager bluetoothMgr = (BluetoothManager) 
                getSystemService(BLUETOOTH_SERVICE);
            mBluetooth = bluetoothMgr.getAdapter();
        }
        if (mBluetooth == null || !mBluetooth.isEnabled()) {
            setState(State.BLUETOOTH_OFF);
        } else {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (mState == State.SCANNING) {
                        mBluetooth.stopLeScan(
                            BleService.this);
                        setState(State.IDLE);
                    }
                }
            }, SCAN_PERIOD);
            mBluetooth.startLeScan(this);
        }
    }
}

从代码中我们不难发现:首先,我们确认了蓝牙服务是否已经开启,如果检测到用户关闭了蓝牙服务,那么就需要去提示用户打开。这一过程的实现非常简单,只要先获取Android蓝牙系统服务BluetoothService的实例对象,然后从这个对象中我们又可以获取到代表蓝牙射频的BluetoothAdapter实例。注意这里需要做一下非空判断,接着可以通过这个Adapter实例的isEnabled()函数来判断蓝牙是否打开并且处于可用状态了。如果服务是不可用状态,那么我们需要定义一个恰当的状态,并且通知给所有监听了服务的客户端,以便于进一步的处理(本文中就是我们的 Activity)。

public class BleActivity extends Activity {
    private final int ENABLE_BT = 1;
    .
    .
    .
    private void enableBluetooth() {
        Intent enableBtIntent = 
            new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, ENABLE_BT);
    }
 
    @Override
    protected void onActivityResult(int requestCode, 
        int resultCode, Intent data) {
        if(requestCode == ENABLE_BT) {
            if(resultCode == RESULT_OK) {
                //Bluetooth connected, we may continue
                startScan();
            } else {
                //The user has elected not to turn on 
                //Bluetooth. There's nothing we can do
                //without it, so let's finish().
                finish();
            }
        } else {
            super.onActivityResult(requestCode, 
                resultCode, data);
        }
    }
 
    private void startScan() {
        mRefreshItem.setEnabled(false);
        mDeviceList.setDevices(this, null);
        mDeviceList.setScanning(true);
        Message msg = Message.obtain(null, 
            BleService.MSG_START_SCAN);
        if (msg != null) {
            try {
                mService.send(msg);
            } catch (RemoteException e) {
                Log.w(TAG, 
                    "Lost connection to service", e);
                unbindService(mConnection);
            }
        }
    }
}

服务端收到消息后,为了提醒用户打开蓝牙,Android系统专门为了这种情况提供了API。我们选择调用这一系统接口,从而保证在不同机型上有较好的原生用户体验。当然,我们也可以通过代码直接去打开蓝牙,但是更推荐的做法是主动去提示用户打开。这样做带来的另一个好处是非常简单的代码实现,我们只需要唤起一个特定的Activity去提示用户操作,而这个Intent Action已经在BluetoothAdapter中定义好了(参考上面代码的7-9行)。当用户操作完成后,我们就会在之前ActivityonActivityResult()方法中,收到处理完成后的消息。

不难发现,至此我们还没有做任何BLE特有的步骤,不过这一些步骤均是使用蓝牙服务所必要的。

之后就是扫描支持BLE的设备了。由于Android为此在BluetoothAdapter中封装了一个名为startLeScan()的方法,而该方法就是用来扫描设备的!所以我们只需要简单的调用该方法就可以开始扫描设备。另外,这一方法需要传入一个BluetoothAdapter.LeScanCallback实例来接受扫描中各种状态的回调。

public class BleService extends Service implements
    BluetoothAdapter.LeScanCallback
    .
    .
    .
    private void startScan() {
        mDevices.clear();
        setState(State.SCANNING);
        if (mBluetooth == null) {
            BluetoothManager bluetoothMgr = (BluetoothManager) 
                getSystemService(BLUETOOTH_SERVICE);
            mBluetooth = bluetoothMgr.getAdapter();
        }
        if (mBluetooth == null || !mBluetooth.isEnabled()) {
            setState(State.BLUETOOTH_OFF);
        } else {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (mState == State.SCANNING) {
                        mBluetooth.stopLeScan(
                            BleService.this);
                        setState(State.IDLE);
                    }
                }
            }, SCAN_PERIOD);
            mBluetooth.startLeScan(this);
        }
    }
 
    @Override
    public void onLeScan(final BluetoothDevice device, 
        int rssi, byte[] scanRecord) {
        if (device != null && !mDevices.containsValue(device) && 
            device.getName() != null && 
            device.getName().equals("SensorTag")) {
            mDevices.put(device.getAddress(), device);
            Message msg = Message.obtain(null, 
                MSG_DEVICE_FOUND);
            if (msg != null) {
                Bundle bundle = new Bundle();
                String[] addresses = mDevices.keySet()
                    .toArray(new String[mDevices.size()]);
                bundle.putStringArray(KEY_MAC_ADDRESSES, 
                    addresses);
                msg.setData(bundle);
                sendMessage(msg);
            }
            Log.d(TAG, "Added " + device.getName() + ": " + 
                device.getAddress());
        }
    }
}

切记,startLeScan()注:原文说是onStartLeScan(),不过Android中并没有这个函数,从上下文意思看应该指startleScan())仅仅只是发起了搜索过程,所以我们必须要记得去停止搜索。在生产使用中,基于不同的需求,一般尽量在找到设备后尽快停止搜索,不过在本文的例子中,我们会通过postDelayed()去定时调用stopLeScan()来停止服务。

在搜索过程中,每次蓝牙Adapter收到任何来自BLE设备的广播信息都会调用BluetoothAdapter.LeScanCallback中的onLeScan()回调。由于在广播模式下的BLE设备会每秒发送10条广播信息,因此在搜索过程中我们要仔细过滤这些冗余信息,保证程序只处理新设备发来的消息。为了达到这个目的,这里通过已发现设备的MAC地址(使用MAC地址在后续会带来一些方便)在onLeScan()中去重,然后把信息保存到一个Map中去。

除了过滤冗余,我们也需要过滤掉那些我们并不关心的设备。通常我们会通过设备的一些特征信息来筛选(后续文章中会详细讲述),不过 SensorTag documentation 建议对于基于 SensorTag 开发的设备我们只需要简单的去匹配设备名为“SensorTag”的设备就可以,本文就选择了这个方式。

每当我们发现一个符合条件的新设备,我们便把这个设备添加到Map中去,同时也把所有已发现设备的MAC地址打包成一个String数组通过MSG_DEVICE_FOUND消息发送给Activity

值得注意的是虽然我们服务中的操作都是在UI线程中执行的,但我们并不需要去担心这会导致UI线程的阻塞。启动BLE搜索的调用是异步执行的,并且会启用一个后台Task来回调onLeScan()。因此,只要我们保证没有在onLeScan()中进行计算密集型的任务,那么是不需要担心这个后台Task会带来任何阻塞的问题。

本文中的Activity也是以状态机的形式来执行,该Activity会根据BleService的状态去刷新改变UI。上方的刷新菜单就是根据BleService是否处于SCANNING状态来切换可用/不可用状态,同时该状态也会使Activity切换到展示已发现设备列表的fragment。 无论何时Activity一收到MSG_DEVICE_FOUND消息就会去刷新已发现设备列表的界面。由于这和BLE并无太大关系,这部分UI刷新的代码文中就不展开说明了,有兴趣的同学可以点击 这里 来访问详细代码。

现在我们已经可以通过这个demo来发现周围处于广播模式的BLE设备,并且在列表中看到这些设备。

discovery

至此,我们完成了基本的“发现设备”功能,之后我们要做的是去连接上其中一个我们发现的设备,这部分会在下一篇文章中讲述。

本文的源码可以在这里下载。

低功耗蓝牙(3),首发于博客 - 伯乐在线