Android藍牙BLE開發(fā)全面總結(jié)

一、藍牙BLE產(chǎn)生背景——藍牙的發(fā)展歷程

要說藍牙BLE的產(chǎn)生背景,首先要放到藍牙的發(fā)展歷程里面去看。說起藍牙,大家一定聽過藍牙1.0、藍牙2.0、藍牙3.0、藍牙4.0,不過現(xiàn)在大部分已經(jīng)不再用版本號區(qū)分藍牙了,藍牙1.0~3.0都是經(jīng)典藍牙,在塞班系統(tǒng)就已經(jīng)開始使用了。什么是經(jīng)典藍牙?它和藍牙BLE有什么區(qū)別?——這就要從頭說起:

藍牙誕生之初,使用的是BR(Basic Rate)技術(shù),此時藍牙的理論傳輸速率,只能達到721.2Kbps。在那個年代,56Kbps的Modem就是高大上了,這個速度可以說是驚為天人了啊!但是科技變化太快了,BR技術(shù)轉(zhuǎn)眼就過時了。那怎么辦呢?縫縫補補一下,增強速度唄,EDR(Enhanced Data Rate)就出現(xiàn)了。使用EDR技術(shù)的藍牙,理論速率可以達到2.1Mbps。這一次的升級換代,還算優(yōu)雅,因為沒有任何的硬件架構(gòu)、軟件架構(gòu)和使用方式上的改變。也許你也猜到了,很快EDR又落伍了,看看人家WIFI(WLAN),幾十Mbps,上百Mbps,咱們才2.1Mbps,也太寒酸了吧!那怎么辦呢?藍牙組織想了個壞主意:哎,WIFI!把你的PHY層和MAC(Media Access Control)層借我用用唄!這就·是AMP(Alternate MAC and PHY layer extension)。艾瑪,終于松口氣了,我們可以達到54Mbps了。不過呢,由于藍牙自身的物理層和AMP技術(shù)差異太明顯了,這次擴展只能是交替使用(Alternate)的,也就是說,有我(BR/EDR)沒你(AMP)。

藍牙配置

這里特別強調(diào)了optional和alternate這兩個字眼,這是藍牙Spec的原話。它意味著,BR和EDR是可以同時存在的,但BR/EDR和AMP只能二選一??偟膩碚f,BR是正宗的藍牙技術(shù),可以包括可選(optional)的EDR技術(shù),以及AMP(交替使用(Alternate)的MAC層和PHY層擴展)

上面所講的藍牙技術(shù)的進化路線,就是傳輸速率的加快、加快、再加快。但能量是守恒的,你想傳的更快,代價就是消耗更多的能量。而有很多的應(yīng)用場景,并不關(guān)心傳輸速率,反而非常關(guān)心功耗。這就是BLE(低功耗藍牙)產(chǎn)生的背景。

有些人一直認為藍牙4.0就是藍牙BLE,其實并不正確。準(zhǔn)確來說4.0是雙模的,既包括經(jīng)典藍牙又包括低功耗藍牙。經(jīng)典藍牙和藍牙BLE雖然都是藍牙,但其實還是存在很大區(qū)別的。藍牙技術(shù)聯(lián)盟在2010年6月30號公布了藍牙4.0標(biāo)準(zhǔn),4.0標(biāo)準(zhǔn)在藍牙3.0+HS標(biāo)準(zhǔn)的基礎(chǔ)上增加了對低功耗藍牙(Bluetooth Low Energy, BLE)的支持。藍牙核心規(guī)范4.0的模塊增加了以下幾個BLE組件:GATTATTSMP(這幾個概念后面會解釋)。

這里插一句題外話:

SIG(藍牙特別利益小組即藍牙官方組織)早已發(fā)布4.1以下舊版本的廢棄時間確定在2019年1月28開始,目前官方建議使用的版本號為藍牙5.0,藍牙6.0,藍牙7.0。

Withdrawal of the following on January 28, 2019:

  • Bluetooth Specification Version 2.0 + EDR

Deprecation of the following on January 28, 2019 and withdrawal on July 1, 2020:

  • Bluetooth Specification Version 2.1 + EDR
  • Bluetooth Specification Version 3.0 + HS
  • Bluetooth Specification Version 4.0
  • Bluetooth Specification Version 4.1

言歸正傳,經(jīng)典藍牙和藍牙BLE都包括搜索(discovery)管理、連接(connection)管理等機制,相互獨立。但是相比傳統(tǒng)藍牙,BLE最大的特點就是低功耗,低延時,快速的搜索和連接速度,但數(shù)據(jù)傳輸速度相比傳統(tǒng)藍牙低,傳輸?shù)臄?shù)據(jù)量也很小,每次只有20個字節(jié)(理論上可以通過一些方法去突破限制,參見藍牙BLE MTU規(guī)則與約定)。BLE技術(shù)相比BR技術(shù),差異非常大,或者說就是兩種不同的技術(shù),湊巧都加一個“藍牙”的前綴而已。藍牙BLE因為其低能耗的優(yōu)點,在智能穿戴設(shè)備和車載系統(tǒng)上的應(yīng)用越來越廣泛。


二、藍牙BLE的基本概念

上面我們知道了藍牙4.0版本中誕生了藍牙BLE,而Android當(dāng)時4.2版本已經(jīng)發(fā)布了,所以真正引入藍牙BLE是在Android4.3系統(tǒng),但是僅作為中央設(shè)備,直到5.0以后才可以既作為中央設(shè)備又可以作為周邊設(shè)備。通俗的說,也就是5.0系統(tǒng)以后,可以手機控制手機了,不過絕大多數(shù)的場景手機還是作為中央設(shè)備去控制其他的周邊設(shè)備。藍牙BLE主要用于手機與周邊設(shè)備進行通信,當(dāng)然也可以用于所有BLE設(shè)備之間的通信。使用BLE可以實現(xiàn)Android與iOS之間的藍牙通信,而普通藍牙卻不可以。

藍牙BLE是基于GATT進行通信的,GATT(Generic Attribute Profile)是一種屬性傳輸協(xié)議,簡單的講可以認為是一種屬性傳輸?shù)膽?yīng)用層協(xié)議。GATT是藍牙4.0特有的Profile通用規(guī)范,BLE應(yīng)用的Profile均基于GATT。GATT定義了一個服務(wù)框架規(guī)范,該框架包括對服務(wù)(Service)和服務(wù)特性(Characteristic)的定義和規(guī)范,和其中讀、寫、通知的特性等??梢詫ATT理解成BLE框架,我們在GATT上面實現(xiàn)BLE功能。

GATT連接是獨占的。也就是一個BLE外設(shè)同時只能被一個中心設(shè)備連接。一旦外設(shè)被連接,它就會馬上停止廣播,這樣它就對其他設(shè)備不可見了。當(dāng)設(shè)備斷開,它又開始廣播。

GATT已經(jīng)成為BLE通信的規(guī)定,每一個設(shè)備中存在很多的Service,Service中還包含有多個Characteristic。在藍牙實際數(shù)據(jù)交換中,就是通過讀寫這些“Characteristic”來實現(xiàn)的。
下圖是GATT中的Service,Characteristic, Descriptor三者之間的關(guān)系圖,在Android的BLE源碼中這三類變量也經(jīng)常出現(xiàn)。

GATT結(jié)構(gòu)圖

結(jié)構(gòu)的組成:

  • 每個BLE設(shè)備由多個Profile(GATT)組成
  • 每個Profile由多個的Service服務(wù)組成
  • 每個Service由多個Characteristic特征組成
  • 每個Characteristic由一個Value值和多個Descriptor描述組成

結(jié)構(gòu)的用途:

  • Service: 是完成一個特定功能的數(shù)據(jù)和行為集合。在Gatt中,一個Service可能包含Service引用以及強制或者可選的Characteristic。
  • Characteristic: 一個Characteristic的定義包含了Characteristic本身,數(shù)值以及描述(Descriptor)的聲明。Characteristic是完成BLE具體功能的基本單位。
  • Value: 是Characteristic的屬性值。
  • Descriptor: 是對Value不同角度的描述和說明,所以有多個Descriptor

圖中畫的比較少,實際上一個藍牙協(xié)議里面包含的Service、Characteristic和Descriptor是比較多的 ,這時候你可能會問,這么多的同名屬性用什么來區(qū)分呢?答案就是——UUID。UUID既有16位的也 有128位的。16位的UUID是經(jīng)過藍牙組織認證的,是需要購買的,而128位的UUID則可以自定義,當(dāng)然也有許多通用的UUID。每個Service、Characteristic或者Descriptor都有一個 128 bit 的UUID來標(biāo)識。但那些被藍牙技術(shù)聯(lián)盟的標(biāo)準(zhǔn)中定義的UUID是以16 bit 來表示的。實際上,16 bit 的UUID,是有附加 Bluetooth Base UUID,即變成0000****-0000-1000-8000-00805f9b34fb(16位UUID被輸入在****的位置)。

Service可以理解為一個功能集合,而Characteristic比較重要,藍牙設(shè)備正是通過Characteristic來進行設(shè)備間的交互的(如讀、寫、通知等操作)。可以這樣來理解這兩個概念:service即面向?qū)ο笾械摹邦悺钡母拍?,characteristic即面向?qū)ο笾小皩傩浴钡母拍睢?/p>

總結(jié)一下就是,藍牙BLE基于GATT協(xié)議傳輸數(shù)據(jù),提供了Serivice和Characteristic進行設(shè)備之間的通訊。這就是藍牙BLE的基本概念。


三、藍牙BLE的架構(gòu)介紹

1. 藍牙BLE架構(gòu)概覽

一般而言,我們把某個協(xié)議的實現(xiàn)代碼稱為協(xié)議棧(protocol stack),BLE協(xié)議棧就是實現(xiàn)低功耗藍牙協(xié)議的代碼,理解和掌握BLE協(xié)議是實現(xiàn)BLE協(xié)議棧的前提。在深入BLE協(xié)議棧各個組成部分之前,我們先看一下BLE協(xié)議棧整體架構(gòu)。

BLE整體架構(gòu)

如上圖所述,要實現(xiàn)一個BLE應(yīng)用,首先需要一個支持BLE射頻的芯片,然后還需要提供一個與此芯片配套的BLE協(xié)議棧,最后在協(xié)議棧上開發(fā)自己的應(yīng)用。可以看出BLE協(xié)議棧是連接芯片和應(yīng)用的橋梁,是實現(xiàn)整個BLE應(yīng)用的關(guān)鍵。那BLE協(xié)議棧具體包含哪些功能呢?簡單來說——BLE協(xié)議棧主要用來對你的應(yīng)用數(shù)據(jù)進行層層封包,以生成一個滿足BLE協(xié)議的空中數(shù)據(jù)包,換句話說,就是把應(yīng)用數(shù)據(jù)包裹在一系列的幀頭(header)和幀尾(tail)中。具體來說,BLE協(xié)議棧主要由如下幾部分組成:

  • PHY層(Physical layer物理層)。PHY層用來指定BLE所用的無線頻段,調(diào)制解調(diào)方式和方法等。PHY層做得好不好,直接決定整個BLE芯片的功耗,靈敏度以及selectivity等射頻指標(biāo)。

  • LL層(Link Layer鏈路層)。LL層是整個BLE協(xié)議棧的核心,也是BLE協(xié)議棧的難點和重點。像Nordic的BLE協(xié)議棧能同時支持20個link(連接),就是LL層的功勞。LL層要做的事情非常多,比如具體選擇哪個射頻通道進行通信,怎么識別空中數(shù)據(jù)包,具體在哪個時間點把數(shù)據(jù)包發(fā)送出去,怎么保證數(shù)據(jù)的完整性,ACK如何接收,如何進行重傳,以及如何對鏈路進行管理和控制等等。LL層只負責(zé)把數(shù)據(jù)發(fā)出去或者收回來,對數(shù)據(jù)進行怎樣的解析則交給上面的GAP或者GATT。

  • HCI(Host controller interface)。HCI是可選的(具體請參考文章: 三種藍牙架構(gòu)實現(xiàn)方案(藍牙協(xié)議棧方案)),HCI主要用于2顆芯片實現(xiàn)BLE協(xié)議棧的場合,用來規(guī)范兩者之間的通信協(xié)議和通信命令等。

  • GAP層(Generic access profile)。GAP是對LL層payload(有效數(shù)據(jù)包)如何進行解析的兩種方式中的一種,而且是最簡單的那一種。GAP簡單的對LL payload進行一些規(guī)范和定義,因此GAP能實現(xiàn)的功能極其有限。GAP目前主要用來進行廣播,掃描和發(fā)起連接等。

  • L2CAP層(Logic link control and adaptation protocol)。L2CAP對LL進行了一次簡單封裝,LL只關(guān)心傳輸?shù)臄?shù)據(jù)本身,L2CAP就要區(qū)分是加密通道還是普通通道,同時還要對連接間隔進行管理。

  • SMP(Secure manager protocol)。SMP用來管理BLE連接的加密和安全的,如何保證連接的安全性,同時不影響用戶的體驗,這些都是SMP要考慮的工作。

  • ATT(Attribute protocol)。簡單來說,ATT層用來定義用戶命令及命令操作的數(shù)據(jù),比如讀取某個數(shù)據(jù)或者寫某個數(shù)據(jù)。BLE協(xié)議棧中,開發(fā)者接觸最多的就是ATT。BLE引入了attribute概念,用來描述一條一條的數(shù)據(jù)。Attribute除了定義數(shù)據(jù),同時定義該數(shù)據(jù)可以使用的ATT命令,因此這一層被稱為ATT層。

  • GATT(Generic attribute profile )。GATT用來規(guī)范attribute中的數(shù)據(jù)內(nèi)容,并運用group(分組)的概念對attribute進行分類管理。沒有GATT,BLE協(xié)議棧也能跑,但互聯(lián)互通就會出問題,也正是因為有了GATT和各種各樣的應(yīng)用profile,BLE擺脫了ZigBee等無線協(xié)議的兼容性困境,成了出貨量最大的2.4G無線通信產(chǎn)品。

我相信很多人看了上面的介紹,還是不懂BLE協(xié)議棧的工作原理,以及每一層具體干什么的,為什么要這么分層。下面我以如何發(fā)送一個數(shù)據(jù)包為例來講解BLE協(xié)議棧各層是如何緊密配合,以完成發(fā)送任務(wù)的。

2. 簡述BLE如何發(fā)送數(shù)據(jù)包

假設(shè)有設(shè)備A和設(shè)備B,設(shè)備A要把自己目前的電量狀態(tài)83%(十六進制表示為0x53)發(fā)給設(shè)備B,該怎么做呢?作為一個開發(fā)者,他希望越簡單越好,對他而言,他希望調(diào)用一個簡單的API就能完成這件事,比如send(0x53),實際上我們的BLE協(xié)議棧就是這樣設(shè)計的,開發(fā)者只需調(diào)用send(0x53)就可以把數(shù)據(jù)發(fā)送出去了,其余的事情BLE協(xié)議棧幫你搞定。很多人會想,BLE協(xié)議棧是不是直接在物理層就把0x53發(fā)出去,就如下圖所示:

這種方式初看起來挺美的,但由于很多細節(jié)沒有考慮到,實際是不可行的。首先,它沒有考慮用哪一個射頻信道來進行傳輸,在不更改API的情況下,我們只能對協(xié)議棧進行分層,為此引入LL層,開發(fā)者還是調(diào)用send(0x53),send(0x53)再調(diào)用send_LL(0x53,2402M)(注:2402M為信道頻率)。這里還有一個問題,設(shè)備B怎么知道這個數(shù)據(jù)包是發(fā)給自己的還是其他人的,為此BLE引入access address概念,用來指明接收者身份,其中,0x8E89BED6這個access address比較特殊,它表示要發(fā)給周邊所有設(shè)備,即廣播。如果你要一對一的進行通信(BLE協(xié)議將其稱為連接),即設(shè)備A的數(shù)據(jù)包只能設(shè)備B接收,同樣設(shè)備B的數(shù)據(jù)包只能設(shè)備A接收,那么就必須生成一個獨特的隨機access address以標(biāo)識設(shè)備A和設(shè)備B兩者之間的連接。

2.1 廣播方式

我們先來看一下簡單的廣播情況,這種情況下,我們把設(shè)備A叫advertiser(廣播者),設(shè)備B叫scanner或者observer(掃描者)。廣播狀態(tài)下設(shè)備A的LL層API將變成send_LL(0x53,2402M, 0x8E89BED6)。由于設(shè)備B可以同時接收到很多設(shè)備的廣播,因此數(shù)據(jù)包還必須包含設(shè)備A的device address(0xE1022AAB753B)以確認該廣播包來自設(shè)備A,為此send_LL參數(shù)需要變成(0x53,2402M, 0x8E89BED6, 0xE1022AAB753B)。LL層還要檢查數(shù)據(jù)的完整性,即數(shù)據(jù)在傳輸過程中有沒有發(fā)生竄改,為此引入CRC24對數(shù)據(jù)包進行檢驗 (假設(shè)為0xB2C78E) 。同時為了調(diào)制解調(diào)電路工作更高效,每一個數(shù)據(jù)包的最前面會加上1個字節(jié)的preamble(前導(dǎo)幀),preamble一般為0x55或者0xAA。這樣,整個空中包就變成(注:空中包用小端模式表示!):

上面這個數(shù)據(jù)包還有如下問題:

  1. 沒有對數(shù)據(jù)包進行分類組織,設(shè)備B無法找到自己想要的數(shù)據(jù)0x53。為此我們需要在access address之后加入兩個字段:LL header和長度字節(jié)。LL header用來表示數(shù)據(jù)包的LL類型,長度字節(jié)用來指明payload的長度
  2. 設(shè)備B什么時候開啟射頻窗口以接收空中數(shù)據(jù)包?如上圖case1所示,當(dāng)設(shè)備A的數(shù)據(jù)包在空中傳輸?shù)臅r候,設(shè)備B把接收窗口關(guān)閉,此時通信將失??;同樣對case2來說,當(dāng)設(shè)備A沒有在空中發(fā)送數(shù)據(jù)包時,設(shè)備B把接收窗口打開,此時通信也將失敗。只有case3的情況,通信才能成功,即設(shè)備A的數(shù)據(jù)包在空中傳輸時,設(shè)備B正好打開射頻接收窗口,此時通信才能成功,換句話說,LL層還必須定義通信時序。
  3. 當(dāng)設(shè)備B拿到數(shù)據(jù)0x53后,該如何解析這個數(shù)據(jù)呢?它到底表示濕度還是電量,還是別的意思?這個就是GAP層要做的工作,GAP層引入了LTV(Length-Type-Value)結(jié)構(gòu)來定義數(shù)據(jù),比如020105,02-長度,01-類型(強制字段,表示廣播flag,廣播包必須包含該字段),05-值。由于廣播包最大只能為31個字節(jié),它能定義的數(shù)據(jù)類型極其有限,像這里說的電量,GAP就沒有定義,因此要通過廣播方式把電量數(shù)據(jù)發(fā)出去,只能使用供應(yīng)商自定義數(shù)據(jù)類型0xFF,即04FF590053,其中04表示長度,F(xiàn)F表示數(shù)據(jù)類型(自定義數(shù)據(jù)),0x0059是供應(yīng)商ID(自定義數(shù)據(jù)中的強制字段),0x53就是我們的數(shù)據(jù)(設(shè)備雙方約定0x53就是表示電量,而不是其他意思)。

最終空中傳輸?shù)臄?shù)據(jù)包將變成:

  • AAD6BE898E600E3B75AB2A02E102010504FF5900538EC7B2
    • AA – 前導(dǎo)幀(preamble)
    • D6BE898E – 訪問地址(access address)
    • 60 – LL幀頭字段(LL header)
    • 0E – 有效數(shù)據(jù)包長度(payload length)
    • 3B75AB2A02E1 – 廣播者設(shè)備地址(advertiser address)
    • 02010504FF590053廣播數(shù)據(jù)
    • 8EC7B2 – CRC24值

有了PHY,LL和GAP,就可以發(fā)送廣播包了,但廣播包攜帶的信息極其有限,而且還有如下幾大限制:

  1. 無法進行一對一雙向通信 (廣播是一對多通信,而且是單方向的通信)
  2. 由于不支持組包和拆包,因此無法傳輸大數(shù)據(jù)
  3. 通信不可靠及效率低下。廣播信道不能太多,否則將導(dǎo)致掃描端效率低下。為此,BLE只使用37(2402MHz) /38(2426MHz) /39(2480MHz)三個信道進行廣播和掃描,因此廣播不支持跳頻。由于廣播是一對多的,所以廣播也無法支持ACK。這些都使廣播通信變得不可靠。
  4. 掃描端功耗高。由于掃描端不知道設(shè)備端何時廣播,也不知道設(shè)備端選用哪個頻道進行廣播,掃描端只能拉長掃描窗口時間,并同時對37/38/39三個通道進行掃描,這樣功耗就會比較高。

而連接則可以很好解決上述問題,下面我們就來看看連接是如何將0x53發(fā)送出去的。

2.2 連接方式

到底什么叫連接(connection)?像有線UART,很容易理解,就是用線(Rx和Tx等)把設(shè)備A和設(shè)備B相連,即為連接。用“線”把兩個設(shè)備相連,實際是讓2個設(shè)備有共同的通信媒介,并讓兩者時鐘同步起來。藍牙連接有何嘗不是這個道理,所謂設(shè)備A和設(shè)備B建立藍牙連接,就是指設(shè)備A和設(shè)備B兩者一對一“同步”成功,其具體包含以下幾方面:

  • 設(shè)備A和設(shè)備B對接下來要使用的物理信道達成一致
  • 設(shè)備A和設(shè)備B雙方建立一個共同的時間錨點,也就是說,把雙方的時間原點變成同一個點
  • 設(shè)備A和設(shè)備B兩者時鐘同步成功,即雙方都知道對方什么時候發(fā)送數(shù)據(jù)包什么時候接收數(shù)據(jù)包
  • 連接成功后,設(shè)備A和設(shè)備B通信流程如下所示:

如上圖所示,一旦設(shè)備A和設(shè)備B連接成功(此種情況下,我們把設(shè)備A稱為Master或者Central,把設(shè)備B稱為Slave或者Peripheral),設(shè)備A將周期性以CI(connection interval)為間隔向設(shè)備B發(fā)送數(shù)據(jù)包,而設(shè)備B也周期性地以CI為間隔打開射頻接收窗口以接收設(shè)備A的數(shù)據(jù)包。同時按照藍牙spec要求,設(shè)備B收到設(shè)備A數(shù)據(jù)包150us后,設(shè)備B切換到發(fā)送狀態(tài),把自己的數(shù)據(jù)發(fā)給設(shè)備A;設(shè)備A則切換到接收狀態(tài),接收設(shè)備B發(fā)過來的數(shù)據(jù)。由此可見,連接狀態(tài)下,設(shè)備A和設(shè)備B的射頻發(fā)送和接收窗口都是周期性地有計劃地開和關(guān),而且開的時間非常短,從而大大降低系統(tǒng)功耗并大大提高系統(tǒng)效率。

現(xiàn)在我們看看連接狀態(tài)下是如何把數(shù)據(jù)0x53發(fā)送出去的,從中大家可以體會到藍牙協(xié)議棧分層的妙處。

  • 對上層開發(fā)者來說,很簡單,他只需要調(diào)用send(0x53)
  • GATT層定義數(shù)據(jù)的類型和分組,方便起見,我們用0x0013表示電量這種數(shù)據(jù)類型,這樣GATT層把數(shù)據(jù)打包成130053(小端模式?。?/li>
  • ATT層用來選擇具體的通信命令,比如讀/寫/notify/indicate等,這里選擇notify命令0x1B,這樣數(shù)據(jù)包變成了:1B130053
  • L2CAP用來指定connection interval(連接間隔),比如每10ms同步一次(CI不體現(xiàn)在數(shù)據(jù)包中),同時指定邏輯通道編號0004(表示ATT命令),最后把ATT數(shù)據(jù)長度0x0004加在包頭,這樣數(shù)據(jù)就變?yōu)椋?40004001B130053
  • LL層要做的工作很多,首先LL層需要指定用哪個物理信道進行傳輸(物理信道不體現(xiàn)在數(shù)據(jù)包中),然后再給此連接分配一個Access address(0x50655DAB)以標(biāo)識此連接只為設(shè)備A和設(shè)備B直連服務(wù),然后加上LL header和payload length字段,LL header標(biāo)識此packet為數(shù)據(jù)packet,而不是control packet等,payload length為整個L2CAP字段的長度,最后加上CRC24字段,以保證整個packet的數(shù)據(jù)完整性,所以數(shù)據(jù)包最后變成:
    • AAAB5D65501E08040004001B130053D550F6
      • AA – 前導(dǎo)幀(preamble)
      • 0x50655DAB – 訪問地址(access address)
      • 1E – LL幀頭字段(LL header)
      • 08 – 有效數(shù)據(jù)包長度(payload length)
      • 04000400 – ATT數(shù)據(jù)長度,以及L2CAP通道編號
      • 1B – notify command
      • 0x0013 – 電量數(shù)據(jù)handle
      • 0x53 – 真正要發(fā)送的電量數(shù)據(jù)
      • 0xF650D5 – CRC24值
      • 雖然上層開發(fā)者只調(diào)用了 send(0x53),但由于藍牙BLE協(xié)議棧層層打包,最后空中實際傳輸?shù)臄?shù)據(jù)將變成下圖所示的模樣,這就既滿足了低功耗藍牙通信的需求,又讓用戶API變得簡單,可謂一箭雙雕!

四、開發(fā)一個BLE應(yīng)用

前面我們講了藍牙BLE的架構(gòu)和內(nèi)部處理邏輯,但是很多上層開發(fā)人員其實并不關(guān)心這些,只想知道如何去開發(fā)一個BLE應(yīng)用。接下來我用一個實例去講一下。

前一段時間有一個項目需求,是做一款智能車鑰匙APP。主要為了在APP上面,通過藍牙BLE消息的發(fā)送與接收,與汽車上裝置的藍牙盒子(下面簡寫成車頂盒)進行無線通信,車頂盒有線接入車機網(wǎng)絡(luò),以實現(xiàn)控制汽車打開關(guān)閉車門、打開關(guān)閉后備箱打開關(guān)閉發(fā)動機等一系列操作。界面很簡單,只有幾個按鍵,仿照的車鑰匙的外觀,保密原則,就不放上來了。

下面大概說一下基本實現(xiàn)思路:

  • APP開放了一個臨時的入口,用于輸入車頂盒的MAC地址,用于自動連接。
  • 系統(tǒng)啟動時,APP中的服務(wù)會在接收到開機廣播后,主動開啟。服務(wù)啟動是會開啟一個線程,在線程中會判斷當(dāng)前APP與車頂盒未連接時,每隔一段時間使用之前保存的車頂盒MAC地址去進行連接操作,知道連接成功為止。
  • 建立BLE連接,點擊主界面按鈕,發(fā)送BLE消息給車頂盒,進行對應(yīng)操作。

接下來從藍牙BLE代碼實現(xiàn)的角度描述一下如何實現(xiàn),先簡單看一下大概流程:

先說一下關(guān)鍵的角色:
BluetoothAdapter
BluetoothAdapter 擁有基本的藍牙操作,例如開啟藍牙掃描,使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)實例化一個 BluetoothDevice 用于連接藍牙設(shè)備的操作等等。

BluetoothDevice
代表一個遠程藍牙設(shè)備。這個類可以讓你連接所代表的藍牙設(shè)備或者獲取一些有關(guān)它的信息,例如它的名字,地址和綁定狀態(tài)等等。

BluetoothGatt
這個類提供了 Bluetooth GATT 的基本功能。例如重新連接藍牙設(shè)備,發(fā)現(xiàn)藍牙設(shè)備的 Service 等等。

BluetoothGattService
這個類通過 BluetoothGatt.getService 獲得,如果當(dāng)前服務(wù)不可見那么將返回一個 null。我們可以通過這個類的 getCharacteristic(UUID uuid) 進一步獲取 Characteristic 實現(xiàn) 藍牙數(shù)據(jù)的雙向傳輸。

BluetoothGattCharacteristic
通過這個類定義需要往外圍設(shè)備寫入的數(shù)據(jù)和讀取外圍設(shè)備發(fā)送過來的數(shù)據(jù)。

4.1 準(zhǔn)備

從硬件工程師手上拿到需要的UUID和MAC地址:

    public static final String MAC = "54:6C:0E:A0:47:5B"; //車頂盒MAC地址
    public static final UUID UUID_SERVICE = UUID.fromString("0000fff0-0000-1000-8000-00805f9b34fb");  //主Service的UUID
    public static final String UUID_CHARA = "0000fff6-0000-1000-8000-00805f9b34fb"; //Characteristic的UUID

4.2 在清單文件中配置權(quán)限

    <!-- 藍牙必須的權(quán)限-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

    <!-- Android6.0及以上必須獲取位置權(quán)限,否則無法掃描到周邊的藍牙設(shè)備  --> 
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <!-- 如果required=true,則應(yīng)用只能在支持BLE的Android設(shè)備上安裝運行,不支持BLE的設(shè)備將finish  --> 
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />

4.3 檢查設(shè)備

首先要檢查定位權(quán)限以及GPS是否開啟

    public static boolean checkGPSPermissions(Activity activity) {
        String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION};
        List<String> permissionDeniedList = new ArrayList<>();
        for (String permission : permissions) {
            int permissionCheck = ContextCompat.checkSelfPermission(activity, permission);
            if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
                onPermissionGranted(activity, permission);
            } else {
                permissionDeniedList.add(permission);
            }
        }
        if (!permissionDeniedList.isEmpty()) {
            String[] deniedPermissions = permissionDeniedList.toArray(new String[permissionDeniedList.size()]);
            ActivityCompat.requestPermissions(activity, deniedPermissions, REQUEST_CODE_PERMISSION_LOCATION);
        }
        return true;
    }
    public static boolean checkGPSIsOpen(Activity activity) {
        LogUtil.d("檢查GPS是否打開");
        LocationManager locationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
        if (locationManager == null)
            return false;
        return locationManager.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER);
    }

然后檢查是否支持藍牙BLE,并開啟藍牙

    public Boolean ensureBLEExists() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            return false;
        }
        //獲取BluetoothAdapter
        BluetoothManager bm = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        if (bm!=null) mBluetoothAdapter = bm.getAdapter();
        // 開啟藍牙
        if (mBluetoothAdapter!=null){
            if (!mBluetoothAdapter.isEnabled()) { //藍牙未開啟,通過隱式意圖請求開啟藍牙
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivityForResult(enableBtIntent, 0);
            }
        }
        return true;
    }

4.4 通過UUID掃描指定的設(shè)備

   public BluetoothAdapter.LeScanCallback mScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
        //device是設(shè)備對象,rssi是信號強度,scanRecord是掃描記錄
            if (device != null) {
              //接口回調(diào)掃描到的設(shè)備
                synchronized (mCallBacks){
                    for (BleAdapterCallBack callBack : mCallBacks) {
                        callBack.onDeviceFound(device, rssi);
                    }
                }
        }
    };

    private void startScan(){
        UUID[] uuid = {UUID_SERVICE };
        if(mIsScanning){ //如果當(dāng)前正在掃描則先停止掃描
            mBluetoothAdapter.stopLeScan(mScanCallback);
        }
        //mBluetoothAdapter.startLeScan(mScanCallback);//不進行特定設(shè)備過濾,掃描所有設(shè)備
        //進行特定uuid過濾,只掃描具有指定Service UUID的設(shè)備
        mBluetoothAdapter.startLeScan(uuid, mScanCallback);
        // 10秒后停止掃描
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //結(jié)束掃描
                mBluetoothAdapter.stopLeScan(mScanCallback);
            }
        },10000);
    }

在 LeScanCallback 回調(diào)的方法中,第一個參數(shù)是代表藍牙設(shè)備的類,可以通過這個類建立藍牙連接獲取關(guān)于這一個設(shè)備的一系列詳細的參數(shù),例如名字,MAC 地址等等;第二個參數(shù)是藍牙的信號強弱指標(biāo),通過藍牙的信號指標(biāo),我們可以大概計算出藍牙設(shè)備離手機的距離。計算公式為:d = 10^((abs(RSSI) - A) / (10 * n))(A:發(fā)射端和接收端相隔1米時的信號強度, n: 環(huán)境衰減因子,A和n的值,需要根據(jù)實際環(huán)境進行檢測得出);第三個參數(shù)是藍牙廣播出來的廣告數(shù)據(jù),包含 廣播數(shù)據(jù) 和 掃描響應(yīng)數(shù)據(jù) (如果有的話),所以長度一般就是 62 字節(jié),BLE4.0規(guī)定,如果廣播包和掃描應(yīng)答包不足字節(jié),則以0補齊。

另外,我們可以調(diào)用mBluetoothAdapter.startLeScan(uuid, mScanCallback),掃描具有指定Service UUID的設(shè)備,也可以調(diào)用mBluetoothAdapter.startLeScan(scanCallback),掃描所有的藍牙設(shè)備,可以根據(jù)不同的方法自行選擇。

藍牙掃描是比較耗費資源的,如果掃描頻率比較高或者時間比較長,在性能差一點手機上會出現(xiàn)電量消耗比較大和發(fā)熱比較嚴(yán)重的情況,所以除非有特別的需求,要設(shè)置適當(dāng)?shù)膾呙钑r間。

4.5 連接設(shè)備

連接藍牙設(shè)備可以通過 BluetoothDevice#ConnectGatt 方法連接,也可以通過 BluetoothGatt#connect 方法進行重新連接。以下分別是兩個方法的官方說明:

BluetoothDevice.connectGatt

BluetoothGatt   connect(Context context, boolean autoConnect, BluetoothGattCallback callback)

第二個參數(shù)表示是否需要自動連接。如果設(shè)置為 true, 表示如果設(shè)備斷開了,會不斷的嘗試自動連接。設(shè)置為 false 表示只進行一次連接嘗試。第三個參數(shù)是連接后進行的一系列操作的回調(diào),例如連接和斷開連接的回調(diào),發(fā)現(xiàn)服務(wù)的回調(diào),成功寫入數(shù)據(jù),成功讀取數(shù)據(jù)的回調(diào)等等。

BluetoothGatt.connect

boolean connect()

調(diào)用這一個方法相當(dāng)與調(diào)用 BluetoothDevice.connectGatt 且第二個參數(shù) autoConnect 設(shè)置為 true。

當(dāng)調(diào)用藍牙的連接方法之后,藍牙會異步執(zhí)行藍牙連接的操作,如果連接成功會回調(diào) BluetoothGattCalback.onConnectionStateChange 方法。這個方法運行的線程是一個 Binder 線程,所以不建議直接在這個線程處理耗時的任務(wù),因為這可能導(dǎo)致藍牙相關(guān)的線程被阻塞。

    //連接狀態(tài)變化的回調(diào)
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        Log.i(TAG, "連接狀態(tài):status:" + status + ",newState:" + newState)
         if (status == BluetoothGatt.GATT_SUCCESS) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                //連接成功,調(diào)用發(fā)現(xiàn)服務(wù)的方法
                gatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.i(TAG, "斷開連接");
                gatt.close();
            }
        } else {
            Log.i(TAG, "連接失?。? + status);
            gatt.close();
        }
    }

這一個方法有三個參數(shù),第一個就藍牙設(shè)備的 Gatt 服務(wù)連接類。第二個參數(shù)代表是否成功執(zhí)行了連接操作,如果為 BluetoothGatt.GATT_SUCCESS 表示成功執(zhí)行連接操作,第三個參數(shù)才有效,否則說明這次連接嘗試不成功。有時候,我們會遇到 status == 133 的情況,根據(jù)網(wǎng)上大部分人的說法,這是因為 Android 最多支持連接 6 到 7 個左右的藍牙設(shè)備,如果超出了這個數(shù)量就無法再連接了。所以當(dāng)我們斷開藍牙設(shè)備的連接時,還必須調(diào)用 BluetoothGatt.close 方法釋放連接資源。否則,在多次嘗試連接藍牙設(shè)備之后很快就會超出這一個限制,導(dǎo)致出現(xiàn)這一個錯誤再也無法連接藍牙設(shè)備。第三個參數(shù)代表當(dāng)前設(shè)備的連接狀態(tài),如果 newState == BluetoothProfile.STATE_CONNECTED 說明設(shè)備已經(jīng)連接,可以進行下一步的操作了(發(fā)現(xiàn)藍牙服務(wù),也就是 Service)。當(dāng)藍牙設(shè)備斷開連接時,這一個方法也會被回調(diào)其中的 newState == BluetoothProfile.STATE_DISCONNECTED。

4.6 獲取GATT服務(wù),進行讀寫通知操作

在成功連接到藍牙設(shè)備之后才能進行這一個步驟,也就是說在 BluetoothGattCallback.onConnectionStateChange 方法被成功回調(diào)且表示成功連接之后調(diào)用 BluetoothGatt.discoverService 這一個方法。當(dāng)這一個方法被調(diào)用之后,系統(tǒng)會異步執(zhí)行發(fā)現(xiàn)服務(wù)的過程,直到 BluetoothGattCallback.onServicesDiscovered 被系統(tǒng)回調(diào)之后,手機設(shè)備和藍牙設(shè)備才算是真正建立了可通信的連接。

    //發(fā)現(xiàn)Service的回調(diào)
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            //if(D) Log.i(TAG, "onServicesDiscovered success.");
            mBluetoothGatt = gatt;
            BluetoothGattService service = gatt.getService(UUID_SERVICE);// 獲取服務(wù)對象
            if (service == null) {
                close();
                return;
            }
                // 獲取BluetoothGattCharactristic
                BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(UUID_CHARA );
        }
    }

當(dāng)我們發(fā)現(xiàn)服務(wù)之后就可以通過 BluetoothGatt.getService 獲取 BluetoothGattService,接著通過 BluetoothGattService.getCharactristic 獲取 BluetoothGattCharactristic。

到這一步,我們已經(jīng)成功和藍牙設(shè)備建立了可通信的連接,接下來就可以執(zhí)行相應(yīng)的藍牙通信操作了,例如寫入數(shù)據(jù),讀取藍牙設(shè)備的數(shù)據(jù)等等。

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

通過 BluetoothGattCharactristic.readCharacteristic 方法可以通知系統(tǒng)去讀取特定的數(shù)據(jù)。如果系統(tǒng)讀取到了藍牙設(shè)備發(fā)送過來的數(shù)據(jù)就會調(diào)用 BluetoothGattCallback.onCharacteristicRead 方法。通過 BluetoothGattCharacteristic.getValue 可以讀取到藍牙設(shè)備的數(shù)據(jù)。以下是代碼示例:

// 讀取數(shù)據(jù)
gatt.readCharacteristic();

// 讀取數(shù)據(jù)回調(diào)
@Override
public void onCharacteristicRead(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,final int status) {
 
    Log.d(TAG, "callback characteristic read status " + status
            + " in thread " + Thread.currentThread());
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, "read value: " + characteristic.getValue());
    }
}
4.6.2 寫入數(shù)據(jù)

和讀取數(shù)據(jù)一樣,在執(zhí)行寫入數(shù)據(jù)前需要獲取到 BluetoothGattCharactristic。接著執(zhí)行一下步驟:

  • 調(diào)用 BluetoothGattCharactristic.setValue 傳入需要寫入的數(shù)據(jù)(藍牙最多單次1支持 20 個字節(jié)數(shù)據(jù)的傳輸,如果需要傳輸?shù)臄?shù)據(jù)大于這一個字節(jié)則需要分包傳輸)。
  • 調(diào)用 BluetoothGattCharactristic.writeCharacteristic 方法通知系統(tǒng)異步往設(shè)備寫入數(shù)據(jù)。
  • 系統(tǒng)回調(diào) BluetoothGattCallback.onCharacteristicWrite 方法通知數(shù)據(jù)已經(jīng)完成寫入。此時,我們需要執(zhí)行 BluetoothGattCharactristic.getValue 方法檢查一下寫入的數(shù)據(jù)是否我們需要發(fā)送的數(shù)據(jù),如果不是按照項目的需要判斷是否需要重發(fā)。
    以下是示例代碼:
// 寫入數(shù)據(jù)
characteristic.setValue(sendValue);
gatt.writeCharacteristic(characteristic);

// 寫入數(shù)據(jù)回調(diào)
@Override
public void onCharacteristicWrite(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,
                                    final int status) {
    Log.d(TAG, "callback characteristic write in thread " + Thread.currentThread());
    if(!characteristic.getValue().equal(sendValue)) {
        // 執(zhí)行重發(fā)策略
        gatt.writeCharacteristic(characteristic);
    }
}
4.6.3 數(shù)據(jù)通知

BLE app通常需要獲取設(shè)備中characteristic 變化的通知。下面的代碼演示了怎么為一個Characteristic 設(shè)置一個監(jiān)聽:

// 注冊數(shù)據(jù)通知監(jiān)聽
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
 
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

// 數(shù)據(jù)通知回調(diào)
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    super.onCharacteristicChanged(gatt, characteristic);
        byte[] data = characteristic.getValue();    //取出接收到的數(shù)據(jù)
}

值得注意的是,除了通過 BluetoothGatt.setCharacteristicNotification 開啟接收通知的開關(guān),還需要往 Characteristic 的 Descriptor 屬性寫入開啟通知的數(shù)據(jù)開關(guān)使得當(dāng)硬件的數(shù)據(jù)改變時,主動往手機發(fā)送數(shù)據(jù)。

4.7 斷開連接

當(dāng)我們連接藍牙設(shè)備完成一系列的藍牙操作之后就可以斷開藍牙設(shè)備的連接了。通過 BluetoothGatt.disconnect 可以斷開正在連接的藍牙設(shè)備。當(dāng)這一個方法被調(diào)用之后,跟connect一樣系統(tǒng)也會異步回調(diào) BluetoothGattCallback.onConnectionStateChange 方法。通過這個方法的 newState 參數(shù)可以判斷是連接成功還是斷開成功的回調(diào)。

由于 Android 藍牙連接設(shè)備的資源有限,當(dāng)我們執(zhí)行斷開藍牙操作之后必須執(zhí)行 BluetoothGatt.close 方法釋放資源。需要注意的是通過 BluetoothGatt.close 方法也可以執(zhí)行斷開藍牙的操作,不過 BluetoothGattCallback.onConnectionStateChange 將不會收到任何回調(diào)。此時如果執(zhí)行 BluetoothGatt.connect 方法會得到一個藍牙 API 的空指針異常。所以,我們推薦的寫法是當(dāng)藍牙成功連接之后,通過 BluetoothGatt.disconnect 斷開藍牙的連接,緊接著在 BluetoothGattCallback.onConnectionStateChange 執(zhí)行 BluetoothGatt.close 方法釋放資源。(代碼見4.5 連接設(shè)備

以上,就是這樣一個需求的簡單介紹,通過這個案例,應(yīng)該可以對一個BLE項目有一個大概的了解。至于架構(gòu)和代碼都是比較簡略甚至很多不合理的細節(jié),大家不必細究。也可以自己動手寫寫,相信會比我寫的完美。

五、結(jié)語

上面就是關(guān)于藍牙BLE的全面總結(jié)。當(dāng)然說是全面,有些夸張,BLE還有很多指的摸索的細節(jié)。藍牙BLE雖然很輕量,但是卻滲透在我們生活的方方面面,隨著技術(shù)的日新月異,藍牙BLE一定會得到更廣泛的應(yīng)用,希望看完這篇總結(jié),大家能夠有所收獲。

之所以說是總結(jié),因為引用的內(nèi)容占了一半。有些來自其他優(yōu)秀的文章,有的來自官方文檔,由于實在查閱了很多的資料,寫到最后已經(jīng)找不到了來源,因此就省略了參考出處,希望不要介意,目的只是總結(jié)和分享,文章對你有些幫助和啟發(fā),就點個贊讓我看到吧。

多謝查看,這里也祝大家元旦快樂!

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

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

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