一、藍牙BLE產生背景——藍牙的發(fā)展歷程
要說藍牙BLE的產生背景,首先要放到藍牙的發(fā)展歷程里面去看。說起藍牙,大家一定聽過藍牙1.0、藍牙2.0、藍牙3.0、藍牙4.0,不過現(xiàn)在大部分已經不再用版本號區(qū)分藍牙了,藍牙1.0~3.0都是經典藍牙,在塞班系統(tǒng)就已經開始使用了。什么是經典藍牙?它和藍牙BLE有什么區(qū)別?——這就要從頭說起:
藍牙誕生之初,使用的是BR(Basic Rate)技術,此時藍牙的理論傳輸速率,只能達到721.2Kbps。在那個年代,56Kbps的Modem就是高大上了,這個速度可以說是驚為天人了啊!但是科技變化太快了,BR技術轉眼就過時了。那怎么辦呢?縫縫補補一下,增強速度唄,EDR(Enhanced Data Rate)就出現(xiàn)了。使用EDR技術的藍牙,理論速率可以達到2.1Mbps。這一次的升級換代,還算優(yōu)雅,因為沒有任何的硬件架構、軟件架構和使用方式上的改變。也許你也猜到了,很快EDR又落伍了,看看人家WIFI(WLAN),幾十Mbps,上百Mbps,咱們才2.1Mbps,也太寒酸了吧!那怎么辦呢?藍牙組織想了個壞主意:哎,WIFI!把你的PHY層和MAC(Media Access Control)層借我用用唄!這就·是AMP(Alternate MAC and PHY layer extension)。艾瑪,終于松口氣了,我們可以達到54Mbps了。不過呢,由于藍牙自身的物理層和AMP技術差異太明顯了,這次擴展只能是交替使用(Alternate)的,也就是說,有我(BR/EDR)沒你(AMP)。

這里特別強調了optional和alternate這兩個字眼,這是藍牙Spec的原話。它意味著,BR和EDR是可以同時存在的,但BR/EDR和AMP只能二選一??偟膩碚f,BR是正宗的藍牙技術,可以包括可選(optional)的EDR技術,以及AMP(交替使用(Alternate)的MAC層和PHY層擴展)。
上面所講的藍牙技術的進化路線,就是傳輸速率的加快、加快、再加快。但能量是守恒的,你想傳的更快,代價就是消耗更多的能量。而有很多的應用場景,并不關心傳輸速率,反而非常關心功耗。這就是BLE(低功耗藍牙)產生的背景。
有些人一直認為藍牙4.0就是藍牙BLE,其實并不正確。準確來說4.0是雙模的,既包括經典藍牙又包括低功耗藍牙。經典藍牙和藍牙BLE雖然都是藍牙,但其實還是存在很大區(qū)別的。藍牙技術聯(lián)盟在2010年6月30號公布了藍牙4.0標準,4.0標準在藍牙3.0+HS標準的基礎上增加了對低功耗藍牙(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

言歸正傳,經典藍牙和藍牙BLE都包括搜索(discovery)管理、連接(connection)管理等機制,相互獨立。但是相比傳統(tǒng)藍牙,BLE最大的特點就是低功耗,低延時,快速的搜索和連接速度,但數據傳輸速度相比傳統(tǒng)藍牙低,傳輸的數據量也很小,每次只有20個字節(jié)(理論上可以通過一些方法去突破限制,參見藍牙BLE MTU規(guī)則與約定)。BLE技術相比BR技術,差異非常大,或者說就是兩種不同的技術,湊巧都加一個“藍牙”的前綴而已。藍牙BLE因為其低能耗的優(yōu)點,在智能穿戴設備和車載系統(tǒng)上的應用越來越廣泛。
二、藍牙BLE的基本概念
上面我們知道了藍牙4.0版本中誕生了藍牙BLE,而Android當時4.2版本已經發(fā)布了,所以真正引入藍牙BLE是在Android4.3系統(tǒng),但是僅作為中央設備,直到5.0以后才可以既作為中央設備又可以作為周邊設備。通俗的說,也就是5.0系統(tǒng)以后,可以手機控制手機了,不過絕大多數的場景手機還是作為中央設備去控制其他的周邊設備。藍牙BLE主要用于手機與周邊設備進行通信,當然也可以用于所有BLE設備之間的通信。使用BLE可以實現(xiàn)Android與iOS之間的藍牙通信,而普通藍牙卻不可以。
藍牙BLE是基于GATT進行通信的,GATT(Generic Attribute Profile)是一種屬性傳輸協(xié)議,簡單的講可以認為是一種屬性傳輸的應用層協(xié)議。GATT是藍牙4.0特有的Profile通用規(guī)范,BLE應用的Profile均基于GATT。GATT定義了一個服務框架規(guī)范,該框架包括對服務(Service)和服務特性(Characteristic)的定義和規(guī)范,和其中讀、寫、通知的特性等??梢詫ATT理解成BLE框架,我們在GATT上面實現(xiàn)BLE功能。
GATT連接是獨占的。也就是一個BLE外設同時只能被一個中心設備連接。一旦外設被連接,它就會馬上停止廣播,這樣它就對其他設備不可見了。當設備斷開,它又開始廣播。
GATT已經成為BLE通信的規(guī)定,每一個設備中存在很多的Service,Service中還包含有多個Characteristic。在藍牙實際數據交換中,就是通過讀寫這些“Characteristic”來實現(xiàn)的。
下圖是GATT中的Service,Characteristic, Descriptor三者之間的關系圖,在Android的BLE源碼中這三類變量也經常出現(xiàn)。

結構的組成:
- 每個BLE設備由多個Profile(GATT)組成
- 每個Profile由多個的Service服務組成
- 每個Service由多個Characteristic特征組成
- 每個Characteristic由一個Value值和多個Descriptor描述組成
結構的用途:
- Service: 是完成一個特定功能的數據和行為集合。在Gatt中,一個Service可能包含Service引用以及強制或者可選的Characteristic。
- Characteristic: 一個Characteristic的定義包含了Characteristic本身,數值以及描述(Descriptor)的聲明。Characteristic是完成BLE具體功能的基本單位。
- Value: 是Characteristic的屬性值。
- Descriptor: 是對Value不同角度的描述和說明,所以有多個Descriptor
圖中畫的比較少,實際上一個藍牙協(xié)議里面包含的Service、Characteristic和Descriptor是比較多的 ,這時候你可能會問,這么多的同名屬性用什么來區(qū)分呢?答案就是——UUID。UUID既有16位的也 有128位的。16位的UUID是經過藍牙組織認證的,是需要購買的,而128位的UUID則可以自定義,當然也有許多通用的UUID。每個Service、Characteristic或者Descriptor都有一個 128 bit 的UUID來標識。但那些被藍牙技術聯(lián)盟的標準中定義的UUID是以16 bit 來表示的。實際上,16 bit 的UUID,是有附加 Bluetooth Base UUID,即變成0000****-0000-1000-8000-00805f9b34fb(16位UUID被輸入在****的位置)。
Service可以理解為一個功能集合,而Characteristic比較重要,藍牙設備正是通過Characteristic來進行設備間的交互的(如讀、寫、通知等操作)。可以這樣來理解這兩個概念:service即面向對象中的“類”的概念,characteristic即面向對象中“屬性”的概念。
總結一下就是,藍牙BLE基于GATT協(xié)議傳輸數據,提供了Serivice和Characteristic進行設備之間的通訊。這就是藍牙BLE的基本概念。
三、藍牙BLE的架構介紹
1. 藍牙BLE架構概覽
一般而言,我們把某個協(xié)議的實現(xiàn)代碼稱為協(xié)議棧(protocol stack),BLE協(xié)議棧就是實現(xiàn)低功耗藍牙協(xié)議的代碼,理解和掌握BLE協(xié)議是實現(xiàn)BLE協(xié)議棧的前提。在深入BLE協(xié)議棧各個組成部分之前,我們先看一下BLE協(xié)議棧整體架構。

如上圖所述,要實現(xiàn)一個BLE應用,首先需要一個支持BLE射頻的芯片,然后還需要提供一個與此芯片配套的BLE協(xié)議棧,最后在協(xié)議棧上開發(fā)自己的應用。可以看出BLE協(xié)議棧是連接芯片和應用的橋梁,是實現(xiàn)整個BLE應用的關鍵。那BLE協(xié)議棧具體包含哪些功能呢?簡單來說——BLE協(xié)議棧主要用來對你的應用數據進行層層封包,以生成一個滿足BLE協(xié)議的空中數據包,換句話說,就是把應用數據包裹在一系列的幀頭(header)和幀尾(tail)中。具體來說,BLE協(xié)議棧主要由如下幾部分組成:
PHY層(Physical layer物理層)。PHY層用來指定BLE所用的無線頻段,調制解調方式和方法等。PHY層做得好不好,直接決定整個BLE芯片的功耗,靈敏度以及selectivity等射頻指標。
LL層(Link Layer鏈路層)。LL層是整個BLE協(xié)議棧的核心,也是BLE協(xié)議棧的難點和重點。像Nordic的BLE協(xié)議棧能同時支持20個link(連接),就是LL層的功勞。LL層要做的事情非常多,比如具體選擇哪個射頻通道進行通信,怎么識別空中數據包,具體在哪個時間點把數據包發(fā)送出去,怎么保證數據的完整性,ACK如何接收,如何進行重傳,以及如何對鏈路進行管理和控制等等。LL層只負責把數據發(fā)出去或者收回來,對數據進行怎樣的解析則交給上面的GAP或者GATT。
HCI(Host controller interface)。HCI是可選的(具體請參考文章: 三種藍牙架構實現(xiàn)方案(藍牙協(xié)議棧方案)),HCI主要用于2顆芯片實現(xiàn)BLE協(xié)議棧的場合,用來規(guī)范兩者之間的通信協(xié)議和通信命令等。
GAP層(Generic access profile)。GAP是對LL層payload(有效數據包)如何進行解析的兩種方式中的一種,而且是最簡單的那一種。GAP簡單的對LL payload進行一些規(guī)范和定義,因此GAP能實現(xiàn)的功能極其有限。GAP目前主要用來進行廣播,掃描和發(fā)起連接等。
L2CAP層(Logic link control and adaptation protocol)。L2CAP對LL進行了一次簡單封裝,LL只關心傳輸的數據本身,L2CAP就要區(qū)分是加密通道還是普通通道,同時還要對連接間隔進行管理。
SMP(Secure manager protocol)。SMP用來管理BLE連接的加密和安全的,如何保證連接的安全性,同時不影響用戶的體驗,這些都是SMP要考慮的工作。
ATT(Attribute protocol)。簡單來說,ATT層用來定義用戶命令及命令操作的數據,比如讀取某個數據或者寫某個數據。BLE協(xié)議棧中,開發(fā)者接觸最多的就是ATT。BLE引入了attribute概念,用來描述一條一條的數據。Attribute除了定義數據,同時定義該數據可以使用的ATT命令,因此這一層被稱為ATT層。
GATT(Generic attribute profile )。GATT用來規(guī)范attribute中的數據內容,并運用group(分組)的概念對attribute進行分類管理。沒有GATT,BLE協(xié)議棧也能跑,但互聯(lián)互通就會出問題,也正是因為有了GATT和各種各樣的應用profile,BLE擺脫了ZigBee等無線協(xié)議的兼容性困境,成了出貨量最大的2.4G無線通信產品。
我相信很多人看了上面的介紹,還是不懂BLE協(xié)議棧的工作原理,以及每一層具體干什么的,為什么要這么分層。下面我以如何發(fā)送一個數據包為例來講解BLE協(xié)議棧各層是如何緊密配合,以完成發(fā)送任務的。
2. 簡述BLE如何發(fā)送數據包
假設有設備A和設備B,設備A要把自己目前的電量狀態(tài)83%(十六進制表示為0x53)發(fā)給設備B,該怎么做呢?作為一個開發(fā)者,他希望越簡單越好,對他而言,他希望調用一個簡單的API就能完成這件事,比如send(0x53),實際上我們的BLE協(xié)議棧就是這樣設計的,開發(fā)者只需調用send(0x53)就可以把數據發(fā)送出去了,其余的事情BLE協(xié)議棧幫你搞定。很多人會想,BLE協(xié)議棧是不是直接在物理層就把0x53發(fā)出去,就如下圖所示:

這種方式初看起來挺美的,但由于很多細節(jié)沒有考慮到,實際是不可行的。首先,它沒有考慮用哪一個射頻信道來進行傳輸,在不更改API的情況下,我們只能對協(xié)議棧進行分層,為此引入LL層,開發(fā)者還是調用send(0x53),send(0x53)再調用send_LL(0x53,2402M)(注:2402M為信道頻率)。這里還有一個問題,設備B怎么知道這個數據包是發(fā)給自己的還是其他人的,為此BLE引入access address概念,用來指明接收者身份,其中,0x8E89BED6這個access address比較特殊,它表示要發(fā)給周邊所有設備,即廣播。如果你要一對一的進行通信(BLE協(xié)議將其稱為連接),即設備A的數據包只能設備B接收,同樣設備B的數據包只能設備A接收,那么就必須生成一個獨特的隨機access address以標識設備A和設備B兩者之間的連接。
2.1 廣播方式
我們先來看一下簡單的廣播情況,這種情況下,我們把設備A叫advertiser(廣播者),設備B叫scanner或者observer(掃描者)。廣播狀態(tài)下設備A的LL層API將變成send_LL(0x53,2402M, 0x8E89BED6)。由于設備B可以同時接收到很多設備的廣播,因此數據包還必須包含設備A的device address(0xE1022AAB753B)以確認該廣播包來自設備A,為此send_LL參數需要變成(0x53,2402M, 0x8E89BED6, 0xE1022AAB753B)。LL層還要檢查數據的完整性,即數據在傳輸過程中有沒有發(fā)生竄改,為此引入CRC24對數據包進行檢驗 (假設為0xB2C78E) 。同時為了調制解調電路工作更高效,每一個數據包的最前面會加上1個字節(jié)的preamble(前導幀),preamble一般為0x55或者0xAA。這樣,整個空中包就變成(注:空中包用小端模式表示!):

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

有了PHY,LL和GAP,就可以發(fā)送廣播包了,但廣播包攜帶的信息極其有限,而且還有如下幾大限制:
- 無法進行一對一雙向通信 (廣播是一對多通信,而且是單方向的通信)
- 由于不支持組包和拆包,因此無法傳輸大數據
- 通信不可靠及效率低下。廣播信道不能太多,否則將導致掃描端效率低下。為此,BLE只使用37(2402MHz) /38(2426MHz) /39(2480MHz)三個信道進行廣播和掃描,因此廣播不支持跳頻。由于廣播是一對多的,所以廣播也無法支持ACK。這些都使廣播通信變得不可靠。
- 掃描端功耗高。由于掃描端不知道設備端何時廣播,也不知道設備端選用哪個頻道進行廣播,掃描端只能拉長掃描窗口時間,并同時對37/38/39三個通道進行掃描,這樣功耗就會比較高。
而連接則可以很好解決上述問題,下面我們就來看看連接是如何將0x53發(fā)送出去的。
2.2 連接方式
到底什么叫連接(connection)?像有線UART,很容易理解,就是用線(Rx和Tx等)把設備A和設備B相連,即為連接。用“線”把兩個設備相連,實際是讓2個設備有共同的通信媒介,并讓兩者時鐘同步起來。藍牙連接有何嘗不是這個道理,所謂設備A和設備B建立藍牙連接,就是指設備A和設備B兩者一對一“同步”成功,其具體包含以下幾方面:
- 設備A和設備B對接下來要使用的物理信道達成一致
- 設備A和設備B雙方建立一個共同的時間錨點,也就是說,把雙方的時間原點變成同一個點
- 設備A和設備B兩者時鐘同步成功,即雙方都知道對方什么時候發(fā)送數據包什么時候接收數據包
- 連接成功后,設備A和設備B通信流程如下所示:

如上圖所示,一旦設備A和設備B連接成功(此種情況下,我們把設備A稱為Master或者Central,把設備B稱為Slave或者Peripheral),設備A將周期性以CI(connection interval)為間隔向設備B發(fā)送數據包,而設備B也周期性地以CI為間隔打開射頻接收窗口以接收設備A的數據包。同時按照藍牙spec要求,設備B收到設備A數據包150us后,設備B切換到發(fā)送狀態(tài),把自己的數據發(fā)給設備A;設備A則切換到接收狀態(tài),接收設備B發(fā)過來的數據。由此可見,連接狀態(tài)下,設備A和設備B的射頻發(fā)送和接收窗口都是周期性地有計劃地開和關,而且開的時間非常短,從而大大降低系統(tǒng)功耗并大大提高系統(tǒng)效率。
現(xiàn)在我們看看連接狀態(tài)下是如何把數據0x53發(fā)送出去的,從中大家可以體會到藍牙協(xié)議棧分層的妙處。
- 對上層開發(fā)者來說,很簡單,他只需要調用send(0x53)
- GATT層定義數據的類型和分組,方便起見,我們用0x0013表示電量這種數據類型,這樣GATT層把數據打包成130053(小端模式?。?/li>
- ATT層用來選擇具體的通信命令,比如讀/寫/notify/indicate等,這里選擇notify命令0x1B,這樣數據包變成了:1B130053
- L2CAP用來指定connection interval(連接間隔),比如每10ms同步一次(CI不體現(xiàn)在數據包中),同時指定邏輯通道編號0004(表示ATT命令),最后把ATT數據長度0x0004加在包頭,這樣數據就變?yōu)椋?40004001B130053
- LL層要做的工作很多,首先LL層需要指定用哪個物理信道進行傳輸(物理信道不體現(xiàn)在數據包中),然后再給此連接分配一個Access address(0x50655DAB)以標識此連接只為設備A和設備B直連服務,然后加上LL header和payload length字段,LL header標識此packet為數據packet,而不是control packet等,payload length為整個L2CAP字段的長度,最后加上CRC24字段,以保證整個packet的數據完整性,所以數據包最后變成:
- AAAB5D65501E08040004001B130053D550F6
- AA – 前導幀(preamble)
- 0x50655DAB – 訪問地址(access address)
- 1E – LL幀頭字段(LL header)
- 08 – 有效數據包長度(payload length)
- 04000400 – ATT數據長度,以及L2CAP通道編號
- 1B – notify command
- 0x0013 – 電量數據handle
- 0x53 – 真正要發(fā)送的電量數據
- 0xF650D5 – CRC24值
- 雖然上層開發(fā)者只調用了 send(0x53),但由于藍牙BLE協(xié)議棧層層打包,最后空中實際傳輸的數據將變成下圖所示的模樣,這就既滿足了低功耗藍牙通信的需求,又讓用戶API變得簡單,可謂一箭雙雕!
- AAAB5D65501E08040004001B130053D550F6

四、開發(fā)一個BLE應用
前面我們講了藍牙BLE的架構和內部處理邏輯,但是很多上層開發(fā)人員其實并不關心這些,只想知道如何去開發(fā)一個BLE應用。接下來我用一個實例去講一下。
前一段時間有一個項目需求,是做一款智能車鑰匙APP。主要為了在APP上面,通過藍牙BLE消息的發(fā)送與接收,與汽車上裝置的藍牙盒子(下面簡寫成車頂盒)進行無線通信,車頂盒有線接入車機網絡,以實現(xiàn)控制汽車打開關閉車門、打開關閉后備箱打開關閉發(fā)動機等一系列操作。界面很簡單,只有幾個按鍵,仿照的車鑰匙的外觀,保密原則,就不放上來了。
下面大概說一下基本實現(xiàn)思路:
- APP開放了一個臨時的入口,用于輸入車頂盒的MAC地址,用于自動連接。
- 系統(tǒng)啟動時,APP中的服務會在接收到開機廣播后,主動開啟。服務啟動是會開啟一個線程,在線程中會判斷當前APP與車頂盒未連接時,每隔一段時間使用之前保存的車頂盒MAC地址去進行連接操作,知道連接成功為止。
- 建立BLE連接,點擊主界面按鈕,發(fā)送BLE消息給車頂盒,進行對應操作。
接下來從藍牙BLE代碼實現(xiàn)的角度描述一下如何實現(xiàn),先簡單看一下大概流程:

先說一下關鍵的角色:
BluetoothAdapter
BluetoothAdapter 擁有基本的藍牙操作,例如開啟藍牙掃描,使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)實例化一個 BluetoothDevice 用于連接藍牙設備的操作等等。
BluetoothDevice
代表一個遠程藍牙設備。這個類可以讓你連接所代表的藍牙設備或者獲取一些有關它的信息,例如它的名字,地址和綁定狀態(tài)等等。
BluetoothGatt
這個類提供了 Bluetooth GATT 的基本功能。例如重新連接藍牙設備,發(fā)現(xiàn)藍牙設備的 Service 等等。
BluetoothGattService
這個類通過 BluetoothGatt.getService 獲得,如果當前服務不可見那么將返回一個 null。我們可以通過這個類的 getCharacteristic(UUID uuid) 進一步獲取 Characteristic 實現(xiàn) 藍牙數據的雙向傳輸。
BluetoothGattCharacteristic
通過這個類定義需要往外圍設備寫入的數據和讀取外圍設備發(fā)送過來的數據。
4.1 準備
從硬件工程師手上拿到需要的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 在清單文件中配置權限
<!-- 藍牙必須的權限-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android6.0及以上必須獲取位置權限,否則無法掃描到周邊的藍牙設備 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 如果required=true,則應用只能在支持BLE的Android設備上安裝運行,不支持BLE的設備將finish -->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
4.3 檢查設備
首先要檢查定位權限以及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掃描指定的設備
public BluetoothAdapter.LeScanCallback mScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
//device是設備對象,rssi是信號強度,scanRecord是掃描記錄
if (device != null) {
//接口回調掃描到的設備
synchronized (mCallBacks){
for (BleAdapterCallBack callBack : mCallBacks) {
callBack.onDeviceFound(device, rssi);
}
}
}
};
private void startScan(){
UUID[] uuid = {UUID_SERVICE };
if(mIsScanning){ //如果當前正在掃描則先停止掃描
mBluetoothAdapter.stopLeScan(mScanCallback);
}
//mBluetoothAdapter.startLeScan(mScanCallback);//不進行特定設備過濾,掃描所有設備
//進行特定uuid過濾,只掃描具有指定Service UUID的設備
mBluetoothAdapter.startLeScan(uuid, mScanCallback);
// 10秒后停止掃描
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//結束掃描
mBluetoothAdapter.stopLeScan(mScanCallback);
}
},10000);
}
在 LeScanCallback 回調的方法中,第一個參數是代表藍牙設備的類,可以通過這個類建立藍牙連接獲取關于這一個設備的一系列詳細的參數,例如名字,MAC 地址等等;第二個參數是藍牙的信號強弱指標,通過藍牙的信號指標,我們可以大概計算出藍牙設備離手機的距離。計算公式為:d = 10^((abs(RSSI) - A) / (10 * n))(A:發(fā)射端和接收端相隔1米時的信號強度, n: 環(huán)境衰減因子,A和n的值,需要根據實際環(huán)境進行檢測得出);第三個參數是藍牙廣播出來的廣告數據,包含 廣播數據 和 掃描響應數據 (如果有的話),所以長度一般就是 62 字節(jié),BLE4.0規(guī)定,如果廣播包和掃描應答包不足字節(jié),則以0補齊。
另外,我們可以調用mBluetoothAdapter.startLeScan(uuid, mScanCallback),掃描具有指定Service UUID的設備,也可以調用mBluetoothAdapter.startLeScan(scanCallback),掃描所有的藍牙設備,可以根據不同的方法自行選擇。
藍牙掃描是比較耗費資源的,如果掃描頻率比較高或者時間比較長,在性能差一點手機上會出現(xiàn)電量消耗比較大和發(fā)熱比較嚴重的情況,所以除非有特別的需求,要設置適當的掃描時間。
4.5 連接設備
連接藍牙設備可以通過 BluetoothDevice#ConnectGatt 方法連接,也可以通過 BluetoothGatt#connect 方法進行重新連接。以下分別是兩個方法的官方說明:
BluetoothDevice.connectGatt
BluetoothGatt connect(Context context, boolean autoConnect, BluetoothGattCallback callback)
第二個參數表示是否需要自動連接。如果設置為 true, 表示如果設備斷開了,會不斷的嘗試自動連接。設置為 false 表示只進行一次連接嘗試。第三個參數是連接后進行的一系列操作的回調,例如連接和斷開連接的回調,發(fā)現(xiàn)服務的回調,成功寫入數據,成功讀取數據的回調等等。
BluetoothGatt.connect
boolean connect()
調用這一個方法相當與調用 BluetoothDevice.connectGatt 且第二個參數 autoConnect 設置為 true。
當調用藍牙的連接方法之后,藍牙會異步執(zhí)行藍牙連接的操作,如果連接成功會回調 BluetoothGattCalback.onConnectionStateChange 方法。這個方法運行的線程是一個 Binder 線程,所以不建議直接在這個線程處理耗時的任務,因為這可能導致藍牙相關的線程被阻塞。
//連接狀態(tài)變化的回調
@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) {
//連接成功,調用發(fā)現(xiàn)服務的方法
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "斷開連接");
gatt.close();
}
} else {
Log.i(TAG, "連接失?。? + status);
gatt.close();
}
}
這一個方法有三個參數,第一個就藍牙設備的 Gatt 服務連接類。第二個參數代表是否成功執(zhí)行了連接操作,如果為 BluetoothGatt.GATT_SUCCESS 表示成功執(zhí)行連接操作,第三個參數才有效,否則說明這次連接嘗試不成功。有時候,我們會遇到 status == 133 的情況,根據網上大部分人的說法,這是因為 Android 最多支持連接 6 到 7 個左右的藍牙設備,如果超出了這個數量就無法再連接了。所以當我們斷開藍牙設備的連接時,還必須調用 BluetoothGatt.close 方法釋放連接資源。否則,在多次嘗試連接藍牙設備之后很快就會超出這一個限制,導致出現(xiàn)這一個錯誤再也無法連接藍牙設備。第三個參數代表當前設備的連接狀態(tài),如果 newState == BluetoothProfile.STATE_CONNECTED 說明設備已經連接,可以進行下一步的操作了(發(fā)現(xiàn)藍牙服務,也就是 Service)。當藍牙設備斷開連接時,這一個方法也會被回調其中的 newState == BluetoothProfile.STATE_DISCONNECTED。
4.6 獲取GATT服務,進行讀寫通知操作
在成功連接到藍牙設備之后才能進行這一個步驟,也就是說在 BluetoothGattCallback.onConnectionStateChange 方法被成功回調且表示成功連接之后調用 BluetoothGatt.discoverService 這一個方法。當這一個方法被調用之后,系統(tǒng)會異步執(zhí)行發(fā)現(xiàn)服務的過程,直到 BluetoothGattCallback.onServicesDiscovered 被系統(tǒng)回調之后,手機設備和藍牙設備才算是真正建立了可通信的連接。
//發(fā)現(xiàn)Service的回調
@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);// 獲取服務對象
if (service == null) {
close();
return;
}
// 獲取BluetoothGattCharactristic
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(UUID_CHARA );
}
}
當我們發(fā)現(xiàn)服務之后就可以通過 BluetoothGatt.getService 獲取 BluetoothGattService,接著通過 BluetoothGattService.getCharactristic 獲取 BluetoothGattCharactristic。
到這一步,我們已經成功和藍牙設備建立了可通信的連接,接下來就可以執(zhí)行相應的藍牙通信操作了,例如寫入數據,讀取藍牙設備的數據等等。
4.6.1 讀取數據
通過 BluetoothGattCharactristic.readCharacteristic 方法可以通知系統(tǒng)去讀取特定的數據。如果系統(tǒng)讀取到了藍牙設備發(fā)送過來的數據就會調用 BluetoothGattCallback.onCharacteristicRead 方法。通過 BluetoothGattCharacteristic.getValue 可以讀取到藍牙設備的數據。以下是代碼示例:
// 讀取數據
gatt.readCharacteristic();
// 讀取數據回調
@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 寫入數據
和讀取數據一樣,在執(zhí)行寫入數據前需要獲取到 BluetoothGattCharactristic。接著執(zhí)行一下步驟:
- 調用 BluetoothGattCharactristic.setValue 傳入需要寫入的數據(藍牙最多單次1支持 20 個字節(jié)數據的傳輸,如果需要傳輸的數據大于這一個字節(jié)則需要分包傳輸)。
- 調用 BluetoothGattCharactristic.writeCharacteristic 方法通知系統(tǒng)異步往設備寫入數據。
- 系統(tǒng)回調 BluetoothGattCallback.onCharacteristicWrite 方法通知數據已經完成寫入。此時,我們需要執(zhí)行 BluetoothGattCharactristic.getValue 方法檢查一下寫入的數據是否我們需要發(fā)送的數據,如果不是按照項目的需要判斷是否需要重發(fā)。
以下是示例代碼:
// 寫入數據
characteristic.setValue(sendValue);
gatt.writeCharacteristic(characteristic);
// 寫入數據回調
@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 數據通知
BLE app通常需要獲取設備中characteristic 變化的通知。下面的代碼演示了怎么為一個Characteristic 設置一個監(jiān)聽:
// 注冊數據通知監(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);
// 數據通知回調
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
byte[] data = characteristic.getValue(); //取出接收到的數據
}
值得注意的是,除了通過 BluetoothGatt.setCharacteristicNotification 開啟接收通知的開關,還需要往 Characteristic 的 Descriptor 屬性寫入開啟通知的數據開關使得當硬件的數據改變時,主動往手機發(fā)送數據。
4.7 斷開連接
當我們連接藍牙設備完成一系列的藍牙操作之后就可以斷開藍牙設備的連接了。通過 BluetoothGatt.disconnect 可以斷開正在連接的藍牙設備。當這一個方法被調用之后,跟connect一樣系統(tǒng)也會異步回調 BluetoothGattCallback.onConnectionStateChange 方法。通過這個方法的 newState 參數可以判斷是連接成功還是斷開成功的回調。
由于 Android 藍牙連接設備的資源有限,當我們執(zhí)行斷開藍牙操作之后必須執(zhí)行 BluetoothGatt.close 方法釋放資源。需要注意的是通過 BluetoothGatt.close 方法也可以執(zhí)行斷開藍牙的操作,不過 BluetoothGattCallback.onConnectionStateChange 將不會收到任何回調。此時如果執(zhí)行 BluetoothGatt.connect 方法會得到一個藍牙 API 的空指針異常。所以,我們推薦的寫法是當藍牙成功連接之后,通過 BluetoothGatt.disconnect 斷開藍牙的連接,緊接著在 BluetoothGattCallback.onConnectionStateChange 執(zhí)行 BluetoothGatt.close 方法釋放資源。(代碼見4.5 連接設備)
以上,就是這樣一個需求的簡單介紹,通過這個案例,應該可以對一個BLE項目有一個大概的了解。至于架構和代碼都是比較簡略甚至很多不合理的細節(jié),大家不必細究。也可以自己動手寫寫,相信會比我寫的完美。
五、結語
上面就是關于藍牙BLE的全面總結。當然說是全面,有些夸張,BLE還有很多指的摸索的細節(jié)。藍牙BLE雖然很輕量,但是卻滲透在我們生活的方方面面,隨著技術的日新月異,藍牙BLE一定會得到更廣泛的應用,希望看完這篇總結,大家能夠有所收獲。
之所以說是總結,因為引用的內容占了一半。有些來自其他優(yōu)秀的文章,有的來自官方文檔,由于實在查閱了很多的資料,寫到最后已經找不到了來源,因此就省略了參考出處,希望不要介意,目的只是總結和分享,文章對你有些幫助和啟發(fā),就點個贊讓我看到吧。
多謝查看,這里也祝大家元旦快樂!