Android ble低功耗藍牙開發(fā)-客戶端

什么是BLE(低功耗藍牙)

BLE(Bluetooth Low Energy蟹但,低功耗藍牙)是對傳統(tǒng)藍牙BR/EDR技術的補充略板。
盡管BLE和傳統(tǒng)藍牙都稱之為藍牙標準亭螟,且共享射頻攀例,但是嘉汰,BLE是一個完全不一樣的技術。
BLE不具備和傳統(tǒng)藍牙BR/EDR的兼容性摩疑。它是專為小數(shù)據(jù)率、離散傳輸?shù)膽枚O計的畏铆。
通信距離上也有改變雷袋,傳統(tǒng)藍牙的傳輸距離幾十米到幾百米不等,BLE則規(guī)定為100米辞居。

概述

在Android4.3(API等級18)平臺上開始支持低功耗藍牙中央設備角色楷怒,而且提供可供應用去發(fā)現(xiàn)服務、查詢服務和讀寫特性的相關API接口瓦灶。與傳統(tǒng)藍牙相比鸠删,低功耗藍牙的設計對電量消耗更低,這允許Android應用與其他的低功耗設備通信時對電量的需求更低贼陶,如距離傳感器刃泡、心率監(jiān)視器和醫(yī)療健康設備等等巧娱。

一、聲明BLE權限

為了在你的應用中使用藍牙功能畏陕,你必須聲明藍牙權限“android.permission.BLUETOOTH”窒百。你需要使用這個權限如執(zhí)行所有的藍牙通信早像,如請求連接,接受連接和傳輸數(shù)據(jù)老翘。

如果想要你的應用去初始化設備發(fā)現(xiàn)或者操縱藍牙設置,你還必須聲明“android.permission.BLUETOOTH_ADMIN”權限锻离。
在應用的AndroidManifest.xml文件中聲明藍牙權限铺峭。如:

   <uses-permission android:name="android.permission.BLUETOOTH"/>
   <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

如果想要聲明你的應用僅對低功耗藍牙是有效的,在app的manifest中還應包含下面這句:

<uses-feature android:name="android.hardware.bluetooth_le"
       android:required="true" /> 

二汽纠、設置BLE

獲取 BluetoothAdapter

所有的藍牙活動都需要藍牙適配器卫键。BluetoothAdapter代表設備本身的藍牙適配器(藍牙無線)。整個系統(tǒng)只有一個藍牙適配器疏虫,而且你的app使用它與系統(tǒng)交互永罚。

  // Initializes Bluetooth adapter.
        final BluetoothManager bluetoothManager =
                (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mBluetoothAdapter = bluetoothManager.getAdapter();
        }
     

開啟藍牙

調(diào)用isEnabled())去檢測藍牙當前是否開啟。如果該方法返回false,藍牙被禁用卧秘。下面的代碼檢查藍牙是否開啟呢袱,如果沒有開啟,將顯示錯誤提示用戶去設置開啟藍牙

   // Ensures Bluetooth is available on the device and it is enabled. If not,
        // displays a dialog requesting user permission to enable Bluetooth.
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);

        }

搜索藍牙設備

為了發(fā)現(xiàn)BLE設備翅敌,使用startLeScan())方法羞福。這個方法需要一個參數(shù)BluetoothAdapter.LeScanCallback。你必須實現(xiàn)它的回調(diào)函數(shù)蚯涮,那就是返回的掃描結(jié)果治专。因為掃描非常消耗電量,你應當遵守以下準則:
只要找到所需的設備遭顶,停止掃描张峰。
不要在循環(huán)里掃描,并且對掃描設置時間限制棒旗。以前可用的設備可能已經(jīng)移出范圍喘批,繼續(xù)掃描消耗電池電量。

 boolean mScanning   = false;
    int     SCAN_PERIOD = 1000;

    /**
     * 定時掃描
     *
     * @param enable
     */
    private void scanLeDevice(final boolean enable) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            // 預先定義停止藍牙掃描的時間(因為藍牙掃描需要消耗較多的電量)
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                        mBluetoothAdapter.stopLeScan(callback);
                    }
                }
            }, SCAN_PERIOD);
            mScanning = true;

            // 定義一個回調(diào)接口供掃描結(jié)束處理
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                mBluetoothAdapter.startLeScan(callback);
            }
        } else {
            mScanning = false;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                mBluetoothAdapter.stopLeScan(callback);
            }
        }
    }

掃描回調(diào)--掃描到可用設備

 List<BluetoothDevice> mBluetoothDeviceList = new ArrayList<>();

    final BluetoothAdapter.LeScanCallback callback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {

            if (device.getName() != null) {
                mBluetoothDeviceList.add(device);
                mMyBlueAdapter.notifyDataSetChanged();
            }

            Log.e(TAG, "run: scanning..." + device.getName() + "," + device.getAddress());
        }
    };

GATT連接

搜索結(jié)束后铣揉,我們可得到一個搜索結(jié)果 BluetoothDevice 饶深,它表示搜到的藍牙設備

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    mBluetoothAdapter.stopLeScan(callback);
                    mBluetoothGatt = connectGatt.connectGatt(MainActivity.this, true, mBluetoothGattCallback);
 }
  • 可以建立一個GATT連接,它需要一個 回調(diào)mBluetoothGattCallback參數(shù)逛拱。
  • 在回調(diào)方法的 onConnectionStateChange 中敌厘,我們可以通過 status 判斷是否GATT連接成功
  • 在GATT連接建立成功后,我們調(diào)用 mBluetoothGatt.discoverServices() 方法 發(fā)現(xiàn)GATT服務朽合。
    如果搜到服務將會觸發(fā)onServicesDiscovered回調(diào)
 public BluetoothGatt mBluetoothGatt;

    //    狀態(tài)改變
    BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);

            Log.e(TAG, "onConnectionStateChange: thread "
                    + Thread.currentThread() + " status " + newState);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                if (status != BluetoothGatt.GATT_SUCCESS) {
                    String err = "Cannot connect device with error status: " + status;
                    // 當嘗試連接失敗的時候調(diào)用 disconnect 方法是不會引起這個方法回調(diào)的俱两,所以這里
                    //   直接回調(diào)就可以了饱狂。
                    gatt.close();
                    Log.e(TAG, err);
                    return;
                }

                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    Log.e(TAG, "Attempting to start service discovery:" +
                            mBluetoothGatt.discoverServices());
                    Log.e(TAG, "connect--->success" + newState + "," + gatt.getServices().size());
                    setState(ConnectionState.STATE_CONNECTING);

                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.e(TAG, "Disconnected from GATT server.");

                    Log.e(TAG, "connect--->failed" + newState);
                    setState(ConnectionState.STATE_NONE);
                }
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "onServicesDiscovered received:  SUCCESS");
                setState(ConnectionState.STATE_CONNECTED);
                initCharacteristic();
                try {
                    Thread.sleep(200);//延遲發(fā)送,否則第一次消息會不成功
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                Log.e(TAG, "onServicesDiscovered error falure " + status);
                setState(ConnectionState.STATE_NONE);
            }

        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
            Log.e(TAG, "onCharacteristicWrite status: " + status);
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
            Log.e(TAG, "onDescriptorWrite status: " + status);
        }

        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorRead(gatt, descriptor, status);
            Log.e(TAG, "onDescriptorRead status: " + status);
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) {
            Log.e(TAG, "onCharacteristicRead status: " + status);
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) {
            Log.e(TAG, "onCharacteristicChanged characteristic: " + characteristic);
            readCharacteristic(characteristic);
        }

    };

發(fā)現(xiàn)服務 (觸發(fā)onServicesDiscovered)

在發(fā)現(xiàn)服務后锋华,會觸發(fā) GATT回調(diào)的onServicesDiscovered 方法嗡官,我們需要在這里初始化我們的操作,包括:

  • 查看服務毯焕⊙苄龋或者便利查找指定的(和目標硬件UUID符合的)服務。
  • 獲得指定服務的特征 characteristicRead 纳猫、characteristicWrite
  • 訂閱“特征”發(fā)生變化的通知”
 public synchronized void initCharacteristic() {
        if (mBluetoothGatt == null)
            throw new NullPointerException();
        List<BluetoothGattService> services = mBluetoothGatt.getServices();
        Log.e(TAG, services.toString());
        BluetoothGattService service = mBluetoothGatt.getService(uuidServer);
        characteristicRead = service.getCharacteristic(uuidCharRead);
        characteristicWrite = service.getCharacteristic(uuidCharWrite);

        if (characteristicRead == null)
            throw new NullPointerException();
        if (characteristicWrite == null)
            throw new NullPointerException();
        mBluetoothGatt.setCharacteristicNotification(characteristicRead, true);
        BluetoothGattDescriptor descriptor = characteristicRead.getDescriptor(uuidDescriptor);
        if (descriptor == null)
            throw new NullPointerException();
        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        mBluetoothGatt.writeDescriptor(descriptor);

    }

訂閱“特征”發(fā)生變化的通知”

調(diào)用 mBluetoothGatt.setCharacteristicNotification() 方法婆咸,傳入一個特征 characteristic 對象。
當這個特征里的數(shù)據(jù)發(fā)生變化(接收到數(shù)據(jù)了)芜辕,會觸發(fā) 回調(diào)方法的 onCharacteristicChanged 方法尚骄。我們在這個回調(diào)方法中讀取數(shù)據(jù)。

        mBluetoothGatt.setCharacteristicNotification(characteristicRead, true);
        BluetoothGattDescriptor descriptor = characteristicRead.getDescriptor(uuidDescriptor);
        if (descriptor == null)
            throw new NullPointerException();
        descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        mBluetoothGatt.writeDescriptor(descriptor);

讀取數(shù)據(jù)

GATT的回調(diào)中有 onCharacteristicChanged 方法侵续,我們在這里可以獲得接收的數(shù)據(jù)
調(diào)用 characteristic.getValue() 方法倔丈,獲得字節(jié)

  public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.e(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.readCharacteristic(characteristic);
        byte[] bytes = characteristic.getValue();
        String str = new String(bytes);
        mHandler.obtainMessage(READ_MESSAGE, str).sendToTarget();
        Log.e(TAG, "## readCharacteristic, 讀取到: " + str);
    }

寫入數(shù)據(jù)

寫入數(shù)據(jù)時,我們需要先獲得特征状蜗,特征存在于服務內(nèi)需五,一般在發(fā)現(xiàn)服務的 onServicesDiscovered 時,查找到特征對象轧坎。

  public void write(byte[] cmd) {
        Log.e(TAG, "write:" + new String(cmd));
        if (cmd == null || cmd.length == 0)
            return;
        //        synchronized (LOCK) {
        characteristicWrite.setValue(cmd);
        characteristicWrite.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
        mBluetoothGatt.writeCharacteristic(characteristicWrite);
        Log.e(TAG, "write:--->" + new String(cmd));
        //        }
    }

關閉藍牙連接

 /**
     * 關閉
     */
    public void close() {
        if (mBluetoothGatt == null) {
            return;
        }
        mBluetoothGatt.close();
        mBluetoothGatt = null;
        setState(ConnectionState.STATE_NONE);
    }

注意

調(diào)用 BluetoothGattCharactristic#setValue 傳入需要寫入的數(shù)據(jù)(藍牙最多單次1支持 20 個字節(jié)數(shù)據(jù)的傳輸宏邮,如果需要傳輸?shù)臄?shù)據(jù)大于這一個字節(jié)則需要分包傳輸)

static void performPeriodicTask( void )
{
  attHandleValueNoti_t noti;
  //dummy handle
  noti.handle = 0x2E;
  noti.len = 20;
  uint8 i;

  for (i= 0; i < 20; i++)
  {
    noti.value[i] = message_counter;
  }
  if (!(GATT_Notification(0, &noti, FALSE))) //if sucessful
  {
    message_counter++;
  }
}

Android ble低功耗藍牙開發(fā)-客戶端
Android ble低功耗藍牙開發(fā)-服務端

源碼傳送門

源碼:

參考文章:

Android BLE 藍牙開發(fā)入門
Android BLE開發(fā)之Android手機與BLE終端通信

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缸血,隨后出現(xiàn)的幾起案子蜜氨,更是在濱河造成了極大的恐慌,老刑警劉巖捎泻,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件飒炎,死亡現(xiàn)場離奇詭異,居然都是意外死亡笆豁,警方通過查閱死者的電腦和手機厌丑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渔呵,“玉大人,你說我怎么就攤上這事砍鸠±┣猓” “怎么了?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵爷辱,是天一觀的道長录豺。 經(jīng)常有香客問我朦肘,道長,這世上最難降的妖魔是什么双饥? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任媒抠,我火速辦了婚禮,結(jié)果婚禮上咏花,老公的妹妹穿的比我還像新娘趴生。我一直安慰自己,他們只是感情好昏翰,可當我...
    茶點故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布苍匆。 她就那樣靜靜地躺著,像睡著了一般棚菊。 火紅的嫁衣襯著肌膚如雪浸踩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天统求,我揣著相機與錄音检碗,去河邊找鬼。 笑死码邻,一個胖子當著我的面吹牛折剃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冒滩,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼微驶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了开睡?” 一聲冷哼從身側(cè)響起因苹,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎篇恒,沒想到半個月后扶檐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡胁艰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年款筑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腾么。...
    茶點故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡奈梳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出解虱,到底是詐尸還是另有隱情攘须,我是刑警寧澤,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布殴泰,位于F島的核電站于宙,受9級特大地震影響浮驳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捞魁,卻給世界環(huán)境...
    茶點故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一至会、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谱俭,春花似錦奉件、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宣吱,卻和暖如春窃这,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背征候。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工杭攻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疤坝。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓兆解,卻偏偏與公主長得像,于是被迫代替她去往敵國和親跑揉。 傳聞我的和親對象是個殘疾皇子锅睛,可洞房花燭夜當晚...
    茶點故事閱讀 43,687評論 2 351