一、藍牙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組件:GATT, ATT和 SMP(這幾個概念后面會解釋)。
這里插一句題外話:
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)。

結(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)。

如上圖所述,要實現(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ù)包還有如下問題:
- 沒有對數(shù)據(jù)包進行分類組織,設(shè)備B無法找到自己想要的數(shù)據(jù)0x53。為此我們需要在access address之后加入兩個字段:LL header和長度字節(jié)。LL header用來表示數(shù)據(jù)包的LL類型,長度字節(jié)用來指明payload的長度
- 設(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層還必須定義通信時序。
- 當(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ā)送廣播包了,但廣播包攜帶的信息極其有限,而且還有如下幾大限制:
- 無法進行一對一雙向通信 (廣播是一對多通信,而且是單方向的通信)
- 由于不支持組包和拆包,因此無法傳輸大數(shù)據(jù)
- 通信不可靠及效率低下。廣播信道不能太多,否則將導(dǎo)致掃描端效率低下。為此,BLE只使用37(2402MHz) /38(2426MHz) /39(2480MHz)三個信道進行廣播和掃描,因此廣播不支持跳頻。由于廣播是一對多的,所以廣播也無法支持ACK。這些都使廣播通信變得不可靠。
- 掃描端功耗高。由于掃描端不知道設(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變得簡單,可謂一箭雙雕!
- AAAB5D65501E08040004001B130053D550F6

四、開發(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ā),就點個贊讓我看到吧。
多謝查看,這里也祝大家元旦快樂!