安卓 藍(lán)牙 android BLE 基礎(chǔ)開(kāi)發(fā)

(本文觀點(diǎn)純屬個(gè)人觀點(diǎn),并非具有權(quán)威參考價(jià)值,如有錯(cuò)誤,望看客指出。)

簡(jiǎn)介

開(kāi)發(fā)之前,首先來(lái)了解一下藍(lán)牙BLE和傳統(tǒng)藍(lán)牙有何區(qū)別,這將會(huì)有助于后面的開(kāi)發(fā)。

  • 在過(guò)去的藍(lán)牙開(kāi)發(fā)項(xiàng)目當(dāng)中,我們?cè)?jīng)使用過(guò)BLUETOOTHSOCKET和BLUETOOTHSERVERSOCKET的方式來(lái)在手機(jī)與藍(lán)牙外設(shè)之間進(jìn)行通信,這種方式與TCP/IP通信有點(diǎn)像,手機(jī)與藍(lán)牙外設(shè)只能其中一個(gè)作為SERVER角色,另外一個(gè)作為CLIENT角色,一般來(lái)說(shuō),誰(shuí)優(yōu)先發(fā)起鏈接請(qǐng)求的,誰(shuí)就是CLIENT,另外一個(gè)就作為SERVER。在通過(guò)BLUETOOTHSOCKET或BLUETOOTHSERVERSOCKET相關(guān)API獲得藍(lán)牙通道(RFCOMM)后,方可利用此通道進(jìn)行雙端通信。

  • 在BLE開(kāi)發(fā)當(dāng)中,同樣存在兩個(gè)角色,一個(gè)是中心角色(CENTRAL),一個(gè)是外設(shè)角色(PERIPHERAL);藍(lán)牙設(shè)備或手機(jī)都可以單獨(dú)作為CENTRAL或PERIPHERAL角色(較舊的版本當(dāng)中手機(jī)無(wú)法作為外設(shè)提供數(shù)據(jù))。外設(shè)角色的作用是為中心角色提供各種數(shù)據(jù),中心角色可以掃描并接收多個(gè)外設(shè)角色數(shù)據(jù),數(shù)據(jù)以服務(wù)(SERVICE)和特征(CHARACTERISTIC)的形式呈現(xiàn)。一個(gè)服務(wù)可以包含多個(gè)特征,如何更好的理解服務(wù)和特征?大家可以把服務(wù)當(dāng)成一個(gè)包裹,特征當(dāng)成包裹里面的物品,包裹有包裹的名字,物品有物品的名字,一個(gè)包裹可以包含多個(gè)物品,或?yàn)榭?。如此一?lái),服務(wù)和特征的特性關(guān)系應(yīng)該更加清晰,我們的目標(biāo)就是用手機(jī)來(lái)獲取外設(shè)的服務(wù),取的其中的特征來(lái)獲得數(shù)據(jù)。

在非常簡(jiǎn)單的介紹過(guò)后,接下來(lái)我們來(lái)看看在BLE開(kāi)發(fā)當(dāng)中各種主要類(lèi)和其作用。

  • BluetoothDeivce:藍(lán)牙設(shè)備,代表一個(gè)具體的藍(lán)牙外設(shè)。
  • BluetoothAdapter:藍(lán)牙適配器,每一臺(tái)支持藍(lán)牙功能的手機(jī)都有一個(gè)藍(lán)牙適配器,一般來(lái)說(shuō),只有一個(gè)。
  • BluetoothManager:藍(lán)牙管理器,主要用于獲取藍(lán)牙適配器和管理所有和藍(lán)牙相關(guān)的東西。
  • BluetoothGatt:通用屬性協(xié)議, 定義了BLE通訊的基本規(guī)則,就是通過(guò)把數(shù)據(jù)包裝成服務(wù)和特征的約定過(guò)程。
  • BluetoothGattCallback:一個(gè)回調(diào)類(lèi),非常重要而且會(huì)頻繁使用,用于回調(diào)GATT通信的各種狀態(tài)和結(jié)果,后面會(huì)詳細(xì)解釋。
  • BluetoothGattCharacteristic:特征,里面包含了一組或多組數(shù)據(jù),是GATT通信中的最小數(shù)據(jù)單元。
  • BluetoothGattService:服務(wù),描述了一個(gè)BLE設(shè)備的一項(xiàng)基本功能,由零或多個(gè)特征組構(gòu)成。
  • BluetoothGattDescriptor:特征描述符,對(duì)特征的額外描述,包括但不僅限于特征的單位,屬性等。
  • BluetoothLeScanner:藍(lán)牙適配器里面的掃描器,用于掃描BLE外設(shè)。

以上就是在BLE開(kāi)發(fā)中一定會(huì)用到的一些基礎(chǔ)類(lèi),實(shí)際使用當(dāng)中,可能還會(huì)用到例如AdvertiseData,AdvertiseSettings,AdvertiseCallback等等,會(huì)在后續(xù)陸續(xù)更新。接下來(lái)需要先簡(jiǎn)單梳理各種類(lèi)和角色之間是什么關(guān)系。

  • 中心與外設(shè)角色:任何設(shè)備都可以單獨(dú)作為中心或外設(shè)角色。一個(gè)沒(méi)有被鏈接的外設(shè)角色,會(huì)向外界發(fā)出廣播,這個(gè)時(shí)候可以被多個(gè)中心角色發(fā)現(xiàn),一旦外設(shè)角色被某個(gè)中心角色鏈接后,外設(shè)角色就會(huì)停止廣播,其他中心角色就無(wú)法在鏈接到這個(gè)外設(shè)角色。中心角色可以掃描外設(shè)角色,可以監(jiān)聽(tīng)接收廣播或主動(dòng)鏈接,一個(gè)中心角色可以與多個(gè)外設(shè)同時(shí)鏈接。
  • 協(xié)議,服務(wù)與特征:一份協(xié)議由一個(gè)或多個(gè)服務(wù)構(gòu)成,一個(gè)服務(wù)由零個(gè)或多個(gè)特征構(gòu)成,一個(gè)特征可以包含一組或多組值,可以包含零個(gè)或多個(gè)描述符。每一個(gè)服務(wù)與特征都有一個(gè)UUID作為唯一識(shí)別符,識(shí)別符有通用的,也可以自定義,也可以隨機(jī)生成,固定格式00000000-0000-0000-0000-000000000000(8-4-4-4-12),一般來(lái)說(shuō)自定義的UUID只有前8位有變化,后面的基本是固定的0000-1000-8000-00805f9b34fb,所以一個(gè)自定義的UUID一般看起來(lái)就像這樣 “0000????-0000-1000-8000-00805f9b34fb” ????就表示4個(gè)16進(jìn)制數(shù)。每一個(gè)特征都有其屬性和權(quán)限(READ | WRITE | NOTIFY | INDICATE),特征根據(jù)屬性可讀可寫(xiě)。
  • 適配器,掃描器,設(shè)備:每一臺(tái)支持藍(lán)牙的手機(jī)中都會(huì)有一個(gè)藍(lán)牙適配器,由藍(lán)牙管理器管理著,從其中獲得藍(lán)牙適配器。適配器中自帶掃描器,使用掃描器可以掃描周邊的藍(lán)牙設(shè)備。

到這里,大體上已經(jīng)對(duì)BLE相關(guān)基礎(chǔ)內(nèi)容有了初步印象,接下來(lái)會(huì)用代碼方式演示如何通過(guò)協(xié)議與外設(shè)通信。

協(xié)議

首先我們來(lái)看看一個(gè)簡(jiǎn)單的BLE協(xié)議是怎么樣定義的。在BLE開(kāi)發(fā)過(guò)程中,硬件開(kāi)發(fā)與軟件開(kāi)發(fā)之間需要互相協(xié)商定義一個(gè)雙方都認(rèn)可的BLE通信協(xié)議,包括所有服務(wù),特征等的UUID,數(shù)據(jù)格式,和數(shù)據(jù)包定義等。先模擬一個(gè)簡(jiǎn)單的協(xié)議,名字叫XXXX服務(wù):

XXXX Service

UUID(Hex) Type(DEFINE) Length Property
0x018f service uuid - -
0x2a19 xxxx characteristic 1 READ
0x4e17 xxxx characteristic 1 NOTIFY

由表中看出,XXXX服務(wù)的服務(wù)UUID是018f (完整的表示 :0000018f - 0000-1000-8000-00805f9b34fb, 在IOS中,可以直接使用018f),其中包含2個(gè)特征,一個(gè)UUID是2a19,屬性為READ可讀,另外一個(gè)是4e17,屬性為NOTIFY通知 。如何用代碼表示一個(gè)已知的UUID?如下:

private UUID xxxxServiceUUID = UUID.fromString("0000018f-0000-1000-8000-00805f9b34fb");

需要注意的是,這里用的String字符串一定是完整的32位的,安卓中不能單獨(dú)使用018f,否則會(huì)報(bào)錯(cuò)。如此一來(lái),一個(gè)服務(wù)UUID就表示完畢了,特征也是一樣。
在藍(lán)牙設(shè)備與中心通信的數(shù)據(jù)交互中,一次傳輸最大只能傳20個(gè)byte,超過(guò)20個(gè)byte,就需要進(jìn)行分包處理了,那么協(xié)議中如何定義這20個(gè)byte當(dāng)中包含的信息呢?舉個(gè)例子,現(xiàn)在我們定義:
byte0-byte1分別表示總包號(hào),分包號(hào);
byte2-byte5表示UCT時(shí)間;

byte0 byte1 byte2 byte3 byte4
0 1 18 62 6e
byte5 byte6 byte7 byte8 byte9
80 0 0 0 0
byte10 byte11 byte12 byte13 byte14
0 0 0 0 0
byte15 byte16 byte17 byte18 byte19
0 0 0 0 0

最后,我們會(huì)收到一條數(shù)據(jù):00 01 18 62 6e 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00
當(dāng)然,不一定需要使用20個(gè)byte,如果6個(gè)就可以解決問(wèn)題,那么我們?yōu)槭裁床挥?00 01 18 62 6e 80 呢?
在中心接收到這條收據(jù)后,我們就可以知道這些數(shù)據(jù)表示:總包號(hào)為0,分包號(hào)為1,UCT時(shí)間為0x18626e80秒,得到這些原始數(shù)據(jù)后,我們就可以根據(jù)具體情況,轉(zhuǎn)換或解析每一個(gè)數(shù)據(jù)表示什么,后面會(huì)講到這些,現(xiàn)在只是舉例說(shuō)明協(xié)議是如何定義的。
這里只是舉例了一個(gè)服務(wù),一個(gè)設(shè)備是可以包含很多個(gè)服務(wù)的,所以,你最后大概會(huì)需要分析一大堆UUID和這些byte數(shù)組。
到此為止,我們已經(jīng)非常簡(jiǎn)單的定義了一個(gè)可用的BLE協(xié)議了。

代碼

在Androidmanifest.xml中添加相關(guān)權(quán)限。

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

<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
<!--當(dāng)android:required為true的時(shí)候,app只能強(qiáng)制運(yùn)行在支持BLE功能的設(shè)備商,為false的時(shí)候,可以運(yùn)行在所有設(shè)備上,-->
<!--但某些方法需要手動(dòng)檢測(cè),否則可能存在隱性BUG-->

新建一個(gè)服務(wù)類(lèi)例如BleService繼承于Service,在服務(wù)里完成對(duì)藍(lán)牙的所有操作。

public class BleService extends Service {
    private BleBinder mBleBinder;
    private Handler mBleHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
    
    @Override
    public IBinder onBind(Intent intent) {
        return mBleBinder;
    }
    
    public class BleBinder extends Binder {
        //在此提供對(duì)外部的調(diào)用方法,當(dāng)某活動(dòng)綁定此服務(wù)后,獲得返回mBleBinder對(duì)象,外部活動(dòng)通過(guò)操作mBleBinder的方法來(lái)控制藍(lán)牙設(shè)備。
        public void startScan(){
        //開(kāi)始掃描......
        }

        public void stopScan(){
        //停止掃描........
        }
        //更多方法........需要注意的是,某些方法為耗時(shí)操作,有必要時(shí)應(yīng)該開(kāi)啟子線程去執(zhí)行。
        //而且藍(lán)牙很多時(shí)候都是異步操作,需要使用許多回調(diào)方法。
        //如果此服務(wù)為獨(dú)立進(jìn)程服務(wù),并為其他app提供數(shù)據(jù),需要注意方法同步。
    }
}

判斷設(shè)備上的藍(lán)牙是否可用,是否開(kāi)啟。

//適配器與藍(lán)牙管理器的成員變量。
private BluetoothAdapter mBluetoothAdapter;
private BluetoothManager mBluetoothManager;

//檢查設(shè)備是否支持BLE功能。
private boolean checkIfSupportBle(){
    return getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}

//如果設(shè)備支持BLE,那么就可以獲取藍(lán)牙適配器。
private BluetoothAdapter getAdapter(){
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
            mBluetoothAdapter = mBluetoothManager.getAdapter();
        } else {
            mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        }
    return mBluetoothAdapter;
}

//獲取完適配器后,需要檢測(cè)是否已經(jīng)打開(kāi)藍(lán)牙功能,如果沒(méi)有,就需要開(kāi)啟。
//開(kāi)啟藍(lán)牙功能需要一小段時(shí)間,具體涉及的線程操作或同步對(duì)象不在此討論,視實(shí)際情況按需編寫(xiě)。
private void enableBluetooth(){
    if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
}

//此方法用于獲取在手機(jī)中已經(jīng)獲取并綁定了的設(shè)備
private void getBoundDevices(){
    Set<BluetoothDevice> boundDevices = mBluetoothAdapter.getBondedDevices();
    for(BluetoothDevice device : boundDevices){
        //對(duì)device進(jìn)行其他操作,比如連接等。
    }
}

掃描的3種方法

//設(shè)備列表成員變量
private List<BluetoothDevice> mDevices;

//startDiscover() 和 startLeScan(),startBleScan() 都可以發(fā)現(xiàn)設(shè)備,但是startLeScan() 和startScan()只會(huì)發(fā)現(xiàn)那些支持BLE的設(shè)備。

//第一種方法
//startDiscover() 是通用掃描方法
private void startDiscover(){
    mBluetoothAdapter.startDiscover();
    //此過(guò)程大概持續(xù)10秒,當(dāng)掃描到藍(lán)牙設(shè)備后,會(huì)發(fā)出廣播,只要在需要的地方注冊(cè)接收廣播,就可以獲得掃描結(jié)果。
    //這種方法可以掃描出所有藍(lán)牙設(shè)備,包括BLE,但貌似不同手機(jī)有不同體驗(yàn),各位實(shí)踐出真知吧,掃不掃得到,看具體設(shè)備。
}
//注冊(cè)此廣播,監(jiān)聽(tīng)BluetoothDevice.ACTION_FOUND,以接收系統(tǒng)消息取得掃描結(jié)果
private class DeviceReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(BluetoothDevice.ACTION_FOUND.equals(action)){
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);  //這個(gè)就是所獲得的藍(lán)牙設(shè)備。
            mDevices.add(device );
        }
    }
}

//第二種方法
//startLeScan是一個(gè)已經(jīng)被聲明過(guò)時(shí)的方法,但此方法依然可用,而且很多設(shè)備上運(yùn)行的還是這個(gè)方法。
//開(kāi)始掃描
private void startLeScan() {
    mBluetoothAdapter.startLeScan(mLeScanCallback);
}
//停止掃描
private void stopScan() {
    mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
//LeScanCallback 是藍(lán)牙掃描返回結(jié)果的回調(diào),可以通過(guò)回調(diào)獲取掃描結(jié)果。
private BluetoothAdapter.LeScanCallback mLeScanCallback= new BluetoothAdapter.LeScanCallback(){
    @Override
    public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
        mDevices.add(bluetoothDevice);
        //成功掃描到設(shè)備后,在這里獲得bluetoothDevice??梢苑胚M(jìn)設(shè)備列表成員變量當(dāng)中方便后續(xù)操作。
        //也可以發(fā)廣播通知activity發(fā)現(xiàn)了新設(shè)備,更新活動(dòng)設(shè)備列表的顯示等。
        //這里需要注意一點(diǎn),在onLeScan當(dāng)中不能執(zhí)行耗時(shí)操作,不宜執(zhí)行復(fù)雜運(yùn)算操作,切記,
        //下面即將提到的onScanResult,onBatchScanResults同理。
    }
};

//第三種方法
//代替已過(guò)時(shí)的startLeScan()方法的方法,但貌有時(shí)候掃不出目標(biāo)設(shè)備。此方法API21以上可用。
//開(kāi)始掃描
private void startBleScan(){
    mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
}
//停止掃描
private void stopBleScan(){
    mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
}
//ScanCallback 是藍(lán)牙掃描返回結(jié)果的回調(diào),可以通過(guò)回調(diào)獲取掃描結(jié)果。
private ScanCallback mScanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);
        //當(dāng)發(fā)現(xiàn)一個(gè)外設(shè)時(shí)回調(diào)此方法,但本人在實(shí)際使用過(guò)程當(dāng)中發(fā)現(xiàn)一些問(wèn)題,
        //此方法在一次掃描過(guò)程當(dāng)中只會(huì)返回一臺(tái)設(shè)備,也就是如果scan有結(jié)果返回后,
        //就會(huì)一直返回被第一次掃描到的那個(gè)設(shè)備,無(wú)論等多久都一樣,所以本人懷疑
        //如果要使用此方法的話,可能需要間歇性多次調(diào)用startScan才能發(fā)現(xiàn)多個(gè)設(shè)備。
        //但是不是這樣,各位可以自己去試一試,因?yàn)楸救嗽陂_(kāi)發(fā)過(guò)程中依然使用了
        //上面第二種過(guò)時(shí)的方法。ScanResult 可以獲得掃描到的設(shè)備,可以保存到設(shè)備列表成員變量當(dāng)中方便后續(xù)操作。
    }

    @Override
    public void onBatchScanResults(List<ScanResult> results) {
        super.onBatchScanResults(results);
        //在此返回一個(gè)包含所有掃描結(jié)果的列表集,包括以往掃描到的結(jié)果。
    }

    @Override
    public void onScanFailed(int errorCode) {
        super.onScanFailed(errorCode);
        //掃描失敗后的處理。
    }
};

//這個(gè)方法意在告訴大家獲取得到設(shè)備后,我們能獲得什么信息,非主要函數(shù),可以忽略。
private void showDetailOfDevice(){
    //獲得設(shè)備名稱(chēng),多個(gè)設(shè)備可以有同一個(gè)名稱(chēng)。
    String deviceName = mTargetDevice.getName();//獲得設(shè)備名稱(chēng),多個(gè)設(shè)備可以有同一個(gè)名稱(chēng)。
    //獲取設(shè)備物理地址,一個(gè)設(shè)備只能有一個(gè)物理地址,每個(gè)設(shè)備都有每個(gè)設(shè)備的物理地址,無(wú)法相同。
    String deviceMacAddress = mTargetDevice.getAddress();
    //綁定設(shè)備
    mTargetDevice.createBond();
    //更多的信息....
}

另外再補(bǔ)充一點(diǎn),對(duì)于安卓6.0以上的基帶,需要獲取動(dòng)態(tài)權(quán)限,具體如下

//android 6.0 以上的掃描結(jié)果獲取動(dòng)態(tài)權(quán)限的方法
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_attach_device);

    //再onCreate方法當(dāng)中加入以下代碼,判斷系統(tǒng)是否需要?jiǎng)討B(tài)獲取權(quán)限。
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
        }
    }
}

//然后加入onRequestPermissionsResult這個(gè)方法,如果只是需要獲取權(quán)限,那么如下就可以了。
//至于獲取到權(quán)限后各位還有什么其他用處,就自由發(fā)揮吧。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

利用以上方法,可以掃描到所需要的藍(lán)牙設(shè)備,需要注意的是,同一時(shí)間只能有一種掃描方法起作用,不能在使用方法1的同時(shí)再使用方法2或3。可以把掃描結(jié)果存儲(chǔ)在一個(gè)成員列表里,以便后續(xù)操作。還有一點(diǎn)需要注意,LeScanCallback和ScanCallback是兩個(gè)不同的類(lèi),注意不要用混淆了。所有掃描方法都會(huì)一定程度影響主線程的流暢性,因此可以考慮把掃描操作放置子線程執(zhí)行。

連接BLE藍(lán)牙設(shè)備

//手機(jī)鏈接藍(lán)牙設(shè)備,就需要獲取與之相關(guān)的GATT鏈接,首先聲明gatt
private BluetoothGatt mBluetoothGatt;
private BluetoothDevice mTargetDevice;//從掃描到的設(shè)備列表里選出目標(biāo)設(shè)備。

//mBluetoothGattCallback 為所有藍(lán)牙數(shù)據(jù)回調(diào)的處理者,也是整個(gè)藍(lán)牙操作當(dāng)中最為核心的一部分
private BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
    //BluetoothGattCallback 里面有很多方法,但并非所有都需要在開(kāi)發(fā)當(dāng)中用到。
    //這里列出來(lái)只是作為部分解析,需要哪個(gè)方法,就重寫(xiě)哪個(gè)方法,不需要的,直接去掉。
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        //當(dāng)設(shè)備與中心連接狀態(tài)發(fā)生改變時(shí)。
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        //當(dāng)發(fā)現(xiàn)設(shè)備服務(wù)時(shí),會(huì)回調(diào)到此處。
    }

    @Override
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        super.onCharacteristicRead(gatt, characteristic, status);
        //讀取特征后回調(diào)到此處。
    }

    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        super.onCharacteristicWrite(gatt, characteristic, status);
        //寫(xiě)入特征后回調(diào)到此處。
    }

    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        super.onCharacteristicChanged(gatt, characteristic);
        //當(dāng)特征(值)發(fā)生變法時(shí)回調(diào)到此處。
    }

    @Override
    public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        super.onDescriptorRead(gatt, descriptor, status);
        //讀取描述符后回調(diào)到此處。
    }

    @Override
    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        super.onDescriptorWrite(gatt, descriptor, status);
        //寫(xiě)入描述符后回調(diào)到此處
    }

    @Override
    public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
        super.onReliableWriteCompleted(gatt, status);
        //暫時(shí)沒(méi)有用過(guò)。
    }

    @Override
    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
        super.onReadRemoteRssi(gatt, rssi, status);
        //Rssi表示設(shè)備與中心的信號(hào)強(qiáng)度,發(fā)生變化時(shí)回調(diào)到此處。
    }

    @Override
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
        super.onMtuChanged(gatt, mtu, status);
        //暫時(shí)沒(méi)有用過(guò)。
    }
};

private void openGatt(){
    mBluetoothGatt = mTargetDevice.connectGatt(BluetoothLeService.this, false, mBluetoothGattCallback);
    //通過(guò)之前掃描所得設(shè)備,打開(kāi)Gatt鏈接。
    //連接可能需要等待。
    //第一個(gè)參數(shù)是傳Context,這個(gè)很好理解。
    //第二個(gè)參數(shù)是控制是否自動(dòng)鏈接,為true的時(shí)候,當(dāng)設(shè)備進(jìn)入中心范圍,會(huì)進(jìn)行自動(dòng)連接,為false反之。
    //第三個(gè)參數(shù)就是上面那個(gè)GattCallback了。
    //此步驟執(zhí)行之后,所有結(jié)果都會(huì)回調(diào)到GattCallback當(dāng)中,接下來(lái)我們就需要對(duì)其進(jìn)行操作了。
}

GattCallback有什么作用?

//特征列表集
private List<BluetoothGattCharacteristic > mCharacteristics;

//在connectGatt()這個(gè)方法執(zhí)行完畢后,GattCallback當(dāng)中便會(huì)出現(xiàn)回調(diào)結(jié)果。
//首先在onConnectionStateChange()這個(gè)方法當(dāng)中會(huì),我們判斷設(shè)備是否成功連接
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    super.onConnectionStateChange(gatt, status, newState);
    switch (newState) {//newState顧名思義,表示當(dāng)前最新?tīng)顟B(tài)。status可以獲取之前的狀態(tài)。
    case BluetoothProfile.STATE_CONNECTED:
        //這里表示已經(jīng)成功連接,如果成功連接,我們就會(huì)執(zhí)行discoverServices()方法去發(fā)現(xiàn)設(shè)備所包含的服務(wù)
        onStateConnected(gatt);
        break;
    case BluetoothProfile.STATE_DISCONNECTED:
        //表示gatt連接已經(jīng)斷開(kāi)。
        onStateDisconnected(gatt);
        break;
    }
}

private void onStateConnected(BluetoothGatt gatt) {
    gatt.discoverServices();
    Log.i("Ble Connection", "Start to discover services.");
}

private void onStateDisconnected(BluetoothGatt gatt) {
    Log.i("Ble Connection", "Connection is broken.");
    gatt.close();
}

//接下來(lái),在執(zhí)行discoverServices()后,外設(shè)就會(huì)告訴我們它能夠?yàn)橹行奶峁┠男┓?wù)
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        //gatt.getServices()可以獲得外設(shè)的所有服務(wù)。
        for (BluetoothGattService service : gatt.getServices()) {//接下來(lái)遍歷所有服務(wù)
            //每發(fā)現(xiàn)一個(gè)服務(wù),我們?cè)俅伪闅v服務(wù)當(dāng)中所包含的特征,service.getCharacteristics()可以獲得當(dāng)前服務(wù)所包含的所有特征
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                mCharacteristics.add(characteristic);//通??梢园阉l(fā)現(xiàn)的特征放進(jìn)一個(gè)列表當(dāng)中以便后續(xù)操作。
                Log.i("", characteristic.getUuid().toString());//打印特征的UUID。
            }
        }
    }
    //當(dāng)方法執(zhí)行完后,我們就獲取了設(shè)備所有的特征了。
    //如果你想知道每個(gè)特征都包含哪些描述符,很簡(jiǎn)單,再用一個(gè)循環(huán)去遍歷每一個(gè)特征的getDescriptor()方法。
}

//當(dāng)我們執(zhí)行了readCharacteristic()方法后,結(jié)果會(huì)回調(diào)在此。
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    super.onCharacteristicRead(gatt, characteristic, status);
    if(status == BluetoothGatt.GATT_SUCCESS) {
        //如果程序執(zhí)行到這里,證明特征的讀取已經(jīng)完成,我們可以在回調(diào)當(dāng)中取出特征的值。
        //特征所包含的值包含在一個(gè)byte數(shù)組內(nèi),我們可以定義一個(gè)臨時(shí)變量來(lái)獲取。

        byte[] characteristicValueBytes = characteristic.getValue();
        //如果這個(gè)特征返回的是一串字符串,那么可以直接獲得其值
        String bytesToString = new String(characteristicValueBytes );

        //如果只需要取得其中的幾個(gè)byte,可以直接指定獲取特定的數(shù)組位置的byte值.
        //例如協(xié)議當(dāng)中定義了這串?dāng)?shù)據(jù)當(dāng)中前2個(gè)byte表示特定一個(gè)數(shù)值,那么獲取這個(gè)值,可以直接寫(xiě)成
        byte[] aValueBytes = new byte[]{ 
            characteristic.getValue()[0], characteristic.getValue()[1]
        }
        Log.i("c-u", "" + Integer.parseInt(UUIDS.bytesToHexString(characteristic.getValue()), 16));
        //至于這個(gè)值時(shí)表示什么,十進(jìn)制數(shù)值?或是一個(gè)字符串?還是翻開(kāi)協(xié)議慢慢找吧。
        //到這里為止,我們已經(jīng)成功采用讀的方式,獲得了存在于特征當(dāng)中的值。
        //characteristic還能為我們提供什么東西呢?屬性,權(quán)限等是比較常用的。
    }
}

//當(dāng)我們執(zhí)行了gatt.setCharacteristicNotification或?qū)懭胩卣鞯臅r(shí)候,結(jié)果會(huì)回調(diào)在此
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    super.onCharacteristicChanged(gatt, characteristic);
    //當(dāng)我們決定用通知的方式獲取外設(shè)特征值的時(shí)候,每當(dāng)特征值發(fā)生變化,程序就會(huì)回調(diào)到此處。
    //在一個(gè)gatt鏈接當(dāng)中,可以同時(shí)存在多個(gè)notify的回調(diào),全部值都會(huì)回調(diào)到這里,那么我們?nèi)绾螀^(qū)分這些值的來(lái)源?
    //這個(gè)時(shí)候,我們就需要去判斷回調(diào)回來(lái)的特征的UUID,因?yàn)閁UID是唯一的,所以我們可以用UUID
    //來(lái)確定,這些數(shù)據(jù)來(lái)自哪個(gè)特征。
    //假設(shè)我們已經(jīng)在BleService當(dāng)中定了多個(gè)我們想要使用的靜態(tài)UUID,前面已經(jīng)說(shuō)過(guò)如何表達(dá)一個(gè)UUID
    //那么我們需要做的就是對(duì)比這些UUID,根據(jù)不同的UUID來(lái)分類(lèi)這些數(shù)據(jù),究竟應(yīng)該交由哪個(gè)方法來(lái)處理
    
    //所以,這么一來(lái)我們便會(huì)發(fā)現(xiàn)其實(shí)上面的onCharacteristicRead也會(huì)出現(xiàn)這種情況,
    //因?yàn)槲覀儾豢赡苤蛔x取一個(gè)特征,除非這個(gè)外設(shè)也只有這一個(gè)特征,
    //究竟是誰(shuí)在讀取,讀取的值來(lái)自于哪個(gè)特征等,都需要進(jìn)行判斷。
    
    if(mUUID_ONE.equals(characteristic.getUuid())){
        //do something.
    } else if (mUUID_TWO.equals(characteristic.getUuid())){
        //do something.
    }
    //我們會(huì)有更好的寫(xiě)法,我們應(yīng)該抽象這些方法,這樣一來(lái)我們可能會(huì)減少很多代碼量。
}

/*從onCharacteristicChanged當(dāng)中我們發(fā)現(xiàn)了一個(gè)問(wèn)題,就是無(wú)論什么時(shí)候,我們都需要去判斷一個(gè)回調(diào)特征
從哪里來(lái)到哪里去,因此我們應(yīng)該采用一種方法來(lái)統(tǒng)一這種讀取的操作*/
private void updateCharacteristic(BluetoothGattCharacteristic characteristic) {
    //在onCharacteristicChanged和onCharacteristicRead方法中直接把characteristic交給updateCharacteristic處理。
    //這樣一來(lái),程序可讀性就會(huì)更加好了,思路也會(huì)更加清晰。
    //這里處理所有的特征取值
    if(mUUID_ONE.equals(characteristic.getUuid())){
        handlerCharacteristic_One();
    } else if (mUUID_TWO.equals(characteristic.getUuid())){
        handlerCharacteristic_Two();
    }
}

private void handlerCharacteristic_One(){
    //相應(yīng)解析方案
}

private void handlerCharacteristic_Two(){
    //相應(yīng)解析方案

}

/*在官方的寫(xiě)法中,采用了Broadcast的方式來(lái)讓Service向其他組件發(fā)送藍(lán)牙回調(diào)數(shù)據(jù),
  這么做當(dāng)然是可以的,但是如果一個(gè)藍(lán)牙設(shè)備包含了很多個(gè)服務(wù)而且服務(wù)當(dāng)中存在大量
  Notify方式讀取的數(shù)據(jù),那么Service收到這些數(shù)據(jù)后,又用Broadcast的方式廣播數(shù)據(jù),
  系統(tǒng)中就會(huì)存在大量廣播而導(dǎo)致app性能下降,如何解決這種問(wèn)題,我們可以使用回調(diào)
  的方式的方式來(lái)觸發(fā)外部activity或fragment等組件取得數(shù)據(jù)更新UI或其他操作,例如我們
  可以這么寫(xiě):
*/

/*我們先定義一個(gè)回調(diào)接口,讓外部Activity或Fragment實(shí)現(xiàn),又或直接新建一個(gè)類(lèi)實(shí)現(xiàn)。
  我們?cè)贏ctivity綁定Service后獲得Binder對(duì)象時(shí),把實(shí)現(xiàn)了ICallback的回調(diào)接口類(lèi)通過(guò)
  setter方式傳入到Service當(dāng)中作為Service的成員變量,例如可在Service的內(nèi)部Binder類(lèi)
  寫(xiě)一個(gè)方法setFragments或setActivities之類(lèi)的。至于回調(diào)的工作方式,這里不做詳盡介紹了。
*/
public interface ICallback {
    //這就是我們定義的接口,讓需要獲得characteristic更新數(shù)據(jù)的外部組件實(shí)現(xiàn)回調(diào)接口。
    void updateCharacteristic(BluetoothGattCharacteristic characteristic);
}

//在Service的GattCallback對(duì)象中,加入一個(gè)方法,如下:
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    super.onCharacteristicChanged(gatt, characteristic);
    updateCharacteristic('這里傳入實(shí)現(xiàn)了ICallback接口的對(duì)象', characteristic);
}

private void updateCharacteristic(ICallback callback, BluetoothGattCharacteristic characteristic) {
    callback.updateCharacteristic(characteristic);
}

//在外部activity或fragment等組件當(dāng)中收到回調(diào)
@Override
public void updateCharacteristic(final BluetoothGattCharacteristic characteristic) {
    /*這里已經(jīng)回到Activity或Fragement了,我們可以獲取characteristic進(jìn)行我們想要的任何操作了,
      但這里需要注意的是,回調(diào)執(zhí)行的線程可能并非UI線程或不是對(duì)該Activity或Fragement當(dāng)中的
      view擁有操作權(quán),可能會(huì)異常,我們需要使用activity或fragment當(dāng)中的handler對(duì)象來(lái)處理*/
    mHandler.post(new Runable(){
        @Override
        public void run(){
            //在這里我們就可以放心執(zhí)行UI更新,例如對(duì)characteristic進(jìn)行g(shù)etValue之類(lèi)的。
        }
    });
}

/*采用回調(diào)的方式可以更加高效快捷地處理藍(lán)牙發(fā)送過(guò)來(lái)的數(shù)據(jù),讓系統(tǒng)中少了一大堆廣播,邏輯也更加
  清晰了。另外也可以通過(guò)觀察者模式來(lái)實(shí)現(xiàn)Service與外部組件交換數(shù)據(jù),讓Serviec作為一個(gè)可以被訂閱
  的對(duì)象(或擁有一個(gè)可被訂閱的對(duì)象),讓想獲得特征更新的外部組件成為訂閱者,這么做也是可以的,
  但依然需要注意線程和使用handler機(jī)制。*/

上面介紹了最常用的2種獲取外設(shè)特征值的回調(diào)函數(shù),read方式與notify方式,一個(gè)相當(dāng)于主動(dòng)獲取,一個(gè)相當(dāng)于被動(dòng)接收。上文中還列出來(lái)GattCallback的其他回調(diào)函數(shù),根據(jù)實(shí)際需要自行添加即可(有時(shí)間會(huì)逐一補(bǔ)全)。

下面介紹如何進(jìn)行對(duì)特征的讀寫(xiě)操作, 要進(jìn)行特征讀寫(xiě),首先需要知道該特征是否可以讀,或是否可寫(xiě)。

//判斷特征可讀
private boolean ifCharacteristicReadable(BluetoothGattCharacteristic characteristic){
    return ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0);
}

//判斷特征可寫(xiě)
private boolean ifCharacteristicWritable(BluetoothGattCharacteristic characteristic){
    return ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 ||
            (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0));
}

//判斷特征是否具備通知屬性
private boolean ifCharacteristicNotifiable (BluetoothGattCharacteristic characteristic){
    return ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0 || 
            (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0));
}

//當(dāng)然,這只是在不知道此特征的讀寫(xiě)性的情況下,才需要做判斷,一般來(lái)說(shuō),協(xié)議中會(huì)明確指出特征的讀寫(xiě)性,
//如果萬(wàn)一真的忘記寫(xiě),我們也可以自己判斷。

在知道了各個(gè)特征的讀寫(xiě)性后,我們終于可以進(jìn)行讀寫(xiě)交互的動(dòng)作了。

//讀取特征,相當(dāng)簡(jiǎn)單,一句話帶過(guò),讀取結(jié)果會(huì)回調(diào)到mGattCallback中的onCharacteristicRead。
private void readCharacteristic(BluetoothGattCharacteristic characteristic){
    mBluetoothGatt.readCharacteristic(characteristic);
}

//寫(xiě)入特征,也相當(dāng)簡(jiǎn)單,一句話帶過(guò),讀取結(jié)果會(huì)回調(diào)到mGattCallback中的onCharacteristicWrite
private void readCharacteristic(BluetoothGattCharacteristic characteristic){
    characteristic.setValue(?);//參數(shù)可以是byte數(shù)組,字符串等。
    mBluetoothGatt.writeCharacteristic(characteristic);
}

//設(shè)置通知,讀取結(jié)果會(huì)回調(diào)到mGattCallback中的onCharacteristicChanged
private void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enable){
    mBluetoothGatt.setCharacteristicNotification(characteristic, enable);
    //以下的幾句代碼有人問(wèn)可不可省略,這里建議寫(xiě)上。
    //在明確知道當(dāng)前特征的描述符前提下,可以直接使用描述符,不需要做判斷,
    //但如果不知道此特征是否具有描述符的情況下,沒(méi)有以下幾行代碼可能會(huì)導(dǎo)致設(shè)置通知失敗的情況發(fā)生。
    List<BluetoothGattDescriptor> descriptorList = characteristic.getDescriptors();
    if (descriptorList != null) {
        for (BluetoothGattDescriptor descriptor : descriptorList) {
            byte[] value = enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE;
            descriptor.setValue(value);
            mBluetoothGatt.writeDescriptor(descriptor);
        }
    }
}

到這里為止,基本上BLE的掃描,連接,讀寫(xiě)都做了一番簡(jiǎn)單介紹了,這里提幾點(diǎn)要點(diǎn)

  • 第一:藍(lán)牙的通信不能進(jìn)行無(wú)間隔讀寫(xiě)發(fā)送操作,如何理解?比如我要連續(xù)設(shè)置2個(gè)CharacteristicNotification為true,當(dāng)代碼這么寫(xiě)的時(shí)候:
//
private void method(){
    setCharacteristicNotification(characteristicOne, true);
    setCharacteristicNotification(characteristicTwo, true);
}

生效的只有characteristicTwo。那么如何解決這種問(wèn)題?我們可以使用Hanlder的機(jī)制,采用PostDeley一個(gè)一個(gè)設(shè)置,也可以在onCharateristicNotify,onCharateristicRead等等Gatt回調(diào)后在回掉函數(shù)中用handler發(fā)message,再在handler中處理(可以把這些任務(wù)抽象成Runnable或Callable,在Gatt收到回調(diào)后再用handler的post方法執(zhí)行下一個(gè)任務(wù),可以考慮把Runnable或Callable放在Queue中,經(jīng)實(shí)際應(yīng)用后,個(gè)人比較推薦后者,就是在Gatt中收到回調(diào)后再執(zhí)行下一個(gè)藍(lán)牙寫(xiě)操作,而不是通過(guò)handler的PostDeley),么一來(lái),我們就有充分時(shí)間讓上一個(gè)設(shè)置生效后,我們?cè)賵?zhí)行下一個(gè)設(shè)置。

  • 第二:鑒于一臺(tái)手機(jī)設(shè)備當(dāng)中,對(duì)Gatt連接的資源做了限制的設(shè)定,一定要注意當(dāng)程序不需要用到連接的時(shí)候,就需要關(guān)閉gatt的連接,讓系統(tǒng)進(jìn)行資源回收。
//斷開(kāi)連接
private void closeGatt(){
    if(mBluetoothGatt != null){
        mBluetoothGatt .disconnect();
    }
}

在GattCallBack的onConnectionStateChange當(dāng)中,關(guān)閉連接

......
case BluetoothProfile.STATE_DISCONNECTED:
    mBluetoothGatt.close();
    mBluetoothGatt = null;
    break;
  • 第三:藍(lán)牙的相關(guān)操作,就以個(gè)人使用經(jīng)歷來(lái)說(shuō),還是放在子線程中執(zhí)行比較穩(wěn)妥,尤其是掃描,當(dāng)然,藍(lán)牙的讀寫(xiě),鏈接放在UI線程并無(wú)不妥。本人目前并沒(méi)有閱讀過(guò)BLE API的源碼,不知道內(nèi)部是如何處理線程的,但為保證UI線程的順暢,最好還是把藍(lán)牙操作放到子線程中執(zhí)行。如日后有時(shí)間閱讀BLE API的源碼后確認(rèn)再更新此說(shuō)法。

好了到這里為止,我們的BleService服務(wù)基本完工,但這只是一個(gè)非常簡(jiǎn)潔的介紹,實(shí)際開(kāi)發(fā)當(dāng)中一定還會(huì)存在各種問(wèn)題,希望大家可以開(kāi)發(fā)順利。

2017-1-11 15:30
2017-3-10 09:54 更新

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Key Terms And Concepts 關(guān)鍵術(shù)語(yǔ)和概念 Here is a summary of key B...
    Jaesoon閱讀 2,560評(píng)論 0 5
  • 前言: 本文主要描述Android BLE的一些基礎(chǔ)知識(shí)及相關(guān)操作流程,不牽扯具體的業(yè)務(wù)實(shí)現(xiàn),其中提供了針對(duì)廣播包...
    幻影宇寰閱讀 5,583評(píng)論 6 19
  • 聲明:轉(zhuǎn)載請(qǐng)注明出處http://www.itdecent.cn/p/54bc88207050 Android 4...
    蛇發(fā)女妖閱讀 6,956評(píng)論 1 4
  • 背景 藍(lán)牙歷史說(shuō)到藍(lán)牙,就不得不說(shuō)下藍(lán)牙技術(shù)聯(lián)盟(Bluetooth SIG),它負(fù)責(zé)藍(lán)牙規(guī)范制定和推廣的國(guó)際組織...
    徐正峰閱讀 13,020評(píng)論 6 33
  • 藍(lán)牙 藍(lán)牙的波段為2400-2483.5MHz(包括防護(hù)頻帶)。這是全球范圍內(nèi)無(wú)需取得執(zhí)照(但定不是無(wú)管制的)的工...
    蘇永茂閱讀 6,575評(píng)論 0 11

友情鏈接更多精彩內(nèi)容