3月中旬跳槽了,一直在新公司「填坑」佃延,看著「先人」寫的代碼现诀,覺得是有改善空間的夷磕,所以這次想聊下這部分內(nèi)容——iOS藍(lán)牙開發(fā)中如何更好地更好地收發(fā)數(shù)據(jù)。
適讀對象:
- 想初步了解iOS藍(lán)牙開發(fā)的朋友(最好連計(jì)算機(jī)基礎(chǔ)都沒有仔沿,就像我這種沒有計(jì)算機(jī)科班基礎(chǔ)的偽程序猿(真文科汪))坐桩;
- 做過藍(lán)牙開發(fā),但是沒有很「優(yōu)雅」地收發(fā)數(shù)據(jù)的朋友(直接用C語言char數(shù)組裝回來,用下標(biāo)索引去取用)封锉。
注意:
- 本文所說的藍(lán)牙绵跷,指BLE(Bluetooth Low Energy/低功耗藍(lán)牙)。一般應(yīng)用蘋果的官方框架CoreBluetooth開發(fā)成福。當(dāng)然碾局,會有不同的第三方框架,最近我做的項(xiàng)目用的就是第三方框架BabyBluetooth奴艾。
- 本文部分代碼净当,有兩種版本,應(yīng)用蘋果框架CoreBluetooth時(shí)蕴潦,用的是Swift像啼。用BabyBluetooth時(shí),用的是Objective-C潭苞。
我們會從哪里拿到數(shù)據(jù)埋合?
我們先簡單回顧一下整個(gè)藍(lán)牙數(shù)據(jù)接收的一般流程:
- 1、藍(lán)牙在不斷地在廣播信號萄传;
- 2、APP掃描蜜猾;
- 3秀菱、發(fā)現(xiàn)設(shè)備(根據(jù)名稱或「服務(wù)」的UUID來辨別是不是我們要連接的設(shè)備);
- 4蹭睡、連接(成功)衍菱;
- 5、調(diào)用方法發(fā)現(xiàn)「服務(wù)」肩豁;
- 6脊串、調(diào)用方法發(fā)現(xiàn)服務(wù)」里的「特征」;
- 7清钥、發(fā)現(xiàn)硬件用于數(shù)據(jù)輸人的「特征」琼锋,保存(APP發(fā)送數(shù)據(jù)給硬件時(shí)要用到這個(gè)「特征」);
- 8祟昭、發(fā)現(xiàn)硬件用于數(shù)據(jù)輸出的「特征」缕坎,進(jìn)行「監(jiān)聽」(硬件就是從這個(gè)「特征」中發(fā)送數(shù)據(jù)給手機(jī)端);
- 9篡悟、利用數(shù)據(jù)輸入「特征」發(fā)送數(shù)據(jù)谜叹,或者等待數(shù)據(jù)輸出「特征」發(fā)出來的數(shù)據(jù)匾寝。
其中第7~8步的代碼(Swift版)如下:
// 第7、8步:
// 發(fā)現(xiàn)特征的回調(diào)(委托)方法(假設(shè)在這之前已經(jīng)「成功連接」荷腊、「發(fā)現(xiàn)服務(wù)」)
func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) {
print("發(fā)現(xiàn)設(shè)備有\(zhòng)(service.characteristics?.count)個(gè)特征, 是:\(service.characteristics)")
// 用for循環(huán)艳悔,找到自己要的特征(以UUID為辨別依據(jù))
for characteristic in service.characteristics! {
switch characteristic.UUID {
// 7、發(fā)現(xiàn)數(shù)據(jù)寫入的特征(我們的硬件是:FF01)
case kCharacteristicDataInUUID:
print("這是用于數(shù)據(jù)寫入的特征,它的UUID是:\(characteristic.UUID)")
// 8女仰、發(fā)現(xiàn)硬件輸出數(shù)據(jù)(APP讀取硬件數(shù)據(jù))的特征(我們的硬件是:FF02)
case kCharacteristicDataOutUUID:
// 監(jiān)聽DataOut特征
print("這是用于讀取數(shù)據(jù)的特征,它的UUID是:\(characteristic.UUID)")
// 8猜年、進(jìn)行監(jiān)聽
peripheral.setNotifyValue(true, forCharacteristic: characteristic)
default:
print("default")
}
}
}
// 第9步:
// 最終,藍(lán)牙發(fā)過來的數(shù)據(jù)董栽,我們會在這個(gè)回調(diào)方法中拿到
func peripheral(peripheral: CBPeripheral, didUpdateNotificationStateForCharacteristic characteristic: CBCharacteristic, error: NSError?) {
print("收到從藍(lán)牙「FFF2特征」發(fā)出的數(shù)據(jù):\(characteristic.value)")
// value是一個(gè)「NSData?」類型的對象
}
所以码倦,我們最終會在peripheral(_:didUpdateNotificationStateForCharacteristic:error:)
方法中拿到數(shù)據(jù)。Objective-C對應(yīng)的方法是peripheral:didUpdateNotificationStateForCharacteristic: error:
注意锭碳,要先用setNotifyValue(_:forCharacteristic characteristic:)
監(jiān)聽對應(yīng)的特征袁稽,才能在上述方法拿到數(shù)據(jù)。
如果在Objective-C中擒抛,會長這樣子(不是官方的框架推汽,用的是BabyBluetooth框架):
// BabyBluetooth這個(gè)框架框架將監(jiān)聽和回調(diào)寫在一起(用Block實(shí)現(xiàn)),能讓代碼不至于那么分散:
// 也就是上面的第8歧沪、9兩步合在一個(gè)方法中了
[_baby notify:peripheral characteristic:_dataOutCharacteristic block:^(CBPeripheral *peripheral, CBCharacteristic *characteristics, NSError *error) {
NSLog(@"收到從藍(lán)牙「FFF2特征」發(fā)出的數(shù)據(jù): %@", characteristics.value);
}
我們會拿到什么樣的數(shù)據(jù)歹撒?
好了,經(jīng)過上面的一系列稍顯繁瑣的步驟诊胞,我們從藍(lán)牙那邊拿到了「NSData?」類型(Objective-C對應(yīng)的是「NSData」類型)的數(shù)據(jù)暖夭。
我們打印一個(gè)「NSData?」對象看看:
print("收到從藍(lán)牙「FFF2特征」發(fā)出的數(shù)據(jù):\(characteristic.value)")
在控制臺,會這樣輸出類似這樣的東西:
收到藍(lán)牙發(fā)出來的數(shù)據(jù): <da13ffff ff640099>
這些是什么鬼撵孤?
這要從NSData說起迈着,NSData是怎么樣的數(shù)據(jù)呢?要經(jīng)過怎么的處理邪码,才能變成我們自己需要的數(shù)據(jù)呢裕菠?
蘋果的官方文檔《Binary Data Programming Guide》中的章節(jié):Accessing and Comparing BytesAccessing and Comparing Bytes說得比較詳細(xì),英文好的朋友可以看看闭专。
我們暫且這樣理解:NSData(NSMutableData)是二進(jìn)制數(shù)據(jù)對象——蘋果將二進(jìn)制數(shù)據(jù)封裝成對象奴潘,讓我們可以用面向?qū)ο蟮乃季S去操作這些數(shù)據(jù)。
我們可以通過原始的二進(jìn)制數(shù)據(jù)(Raw Bytes)去生成NSData對象影钉,也可以通過NSData存取/訪問(Accessing)這些二進(jìn)制數(shù)據(jù)画髓。
你在逗我么?說好的二進(jìn)制數(shù)據(jù)呢平委?不應(yīng)該全部是0雀扶、1么?為什么會有d啊、a啊愚墓、f啊予权,罩杯么?
莫生氣浪册,<da13ffff ff640099>
只是用十六進(jìn)制呈現(xiàn)給我們而已扫腺,也就是0xda、0x13村象、0xff笆环、0xff、0xff厚者、0x64躁劣、0x00濒憋、0x99眼虱,藍(lán)牙傳了這8個(gè)十六進(jìn)制的數(shù)(8個(gè)byte)給我們磷斧。
為什么不直接用二進(jìn)制乎芳?好,我知道你不死心的朦乏,二進(jìn)制是這樣的:<11011010 00010011 11111111 11111111 11111111 01100100 00000000 10011001>
暈沒有顾翼?你要繼續(xù)堅(jiān)持用二進(jìn)制嗎芋忿?「阿爾法狗」倒應(yīng)該是很樂意的烫止。
正因?yàn)?a target="_blank" rel="nofollow">二進(jìn)制與十六進(jìn)制之間的轉(zhuǎn)換比較簡單蒋荚,所以在計(jì)算機(jī)領(lǐng)域,16進(jìn)制比較通用馆蠕。這就解釋了為什么我們打印出來的NSData對象最終以十六進(jìn)制方式呈現(xiàn)(上面才僅僅是8個(gè)byte的0和1期升。1KB=1024Bytes,給你0.5KB的0和1互躬,十副老花鏡都看不過來)播赁。
這些數(shù)據(jù)有什么意義(表示什么)?
這個(gè)問題問得好吨铸,這個(gè)問題就好比如:「雞」為什么叫「雞」,「鴨」為什么叫「鴨」祖秒?(好不搭邊的比喻~)
其實(shí)是這樣的诞吱,很久很久以前,第一個(gè)發(fā)現(xiàn)「雞」這個(gè)物種的中國人竭缝,他腦洞不知道為什么就浮現(xiàn)了「雞」這個(gè)字房维,于是很隨機(jī)地用「雞」這個(gè)「符號」把它「定義」為「雞」。如果你能穿越回去抬纸,完全可以讓他用「鴨」這個(gè)「符號」的咙俩,如果真是那樣,現(xiàn)在的「雞」就不是「雞」,「鴨」就不是「鴨」了阿趁,而應(yīng)該是「雞」是「鴨」膜蛔,「鴨」是「雞」……是不是有點(diǎn)暈?放心脖阵,以目前的科技水平皂股,你是沒辦法穿越回去的,所以命黔,「雞」還是「雞」呜呐,「鴨」還是「鴨」。
言歸正傳悍募,所以這8個(gè)十六進(jìn)制數(shù)據(jù)表示什么蘑辑,完全取決于我們自己的「定義」,程序猿們會把這種「定義」叫做「協(xié)議」坠宴,也有叫「指令」的洋魂。請看下圖,這就是其中一個(gè)聰明的猿類「定義」的一條指令:
- 第1個(gè)字節(jié)表示起始位啄踊;
- 第2個(gè)字節(jié)是指令號忧设,用于識別是哪一條指令;
- 第3-4個(gè)字節(jié),表示的是顏色值(分別代表RGB三原色其中一色);
- 第6個(gè)字節(jié)表示亮度值;
- 第7個(gè)字節(jié)是保留位颠通,作用是如果突然要增加內(nèi)容址晕,有位置可加;
- 第8個(gè)字節(jié)是校驗(yàn)位顿锰,用于確保整條指令的完整性(可以是固定值谨垃,也可以通過一定的算法算出,這里是使用固定值)硼控,大概意思就是:見到0x99刘陶,就表示這是一條完整的指令了。
備注:這里的「MCU to Phone」牢撼,表示這條數(shù)據(jù)是從硬件(單片機(jī))發(fā)送到手機(jī)的匙隔。
所以,你從藍(lán)牙接收到的數(shù)據(jù)熏版,不要問我有什么意義纷责,表示的是什么。應(yīng)該問寫固件撼短、作定義的同事再膳,或者是寫APP的和寫固件的同事一起定義——往往固件的同事單獨(dú)定義,對寫APP的同事來說曲横,會有很多坑喂柒,因?yàn)樗麄兒茈y考慮得到APP這邊的情況(深受其害狀)。
如何更好地收發(fā)數(shù)據(jù)
好了,上面講了一大堆灾杰,終于要和標(biāo)題扯上點(diǎn)關(guān)系了蚊丐。
拿上面的收到的這條指令舉例,或許你已經(jīng)發(fā)現(xiàn)吭露,對我們有意義的數(shù)據(jù)吠撮,其實(shí)就是byte3~byte6這4個(gè)字節(jié),前3個(gè)是顏色值讲竿,最后1個(gè)是亮度值(其實(shí)這是一個(gè)利用藍(lán)牙泥兰,用手機(jī)APP控制燈具顏色、亮度的產(chǎn)品题禀。這條指令是從硬件(Device to Mobile)獲取顏色鞋诗、亮度值)。
我們當(dāng)然可以簡單粗暴直接地聲明一個(gè)可以容納若干個(gè)元素的C語言數(shù)組(buffer)迈嘹,來接收這8bytes數(shù)據(jù)(我所在公司的前同事也的確是這樣做的),類似如下流程:
// 會聲明一個(gè)可以容納若干個(gè)元素的C數(shù)組(類型一般是無符號的char類型)
// 在OC中削彬,UInt8、uint8_t都是unsigned char
UInt8 tmpBuffer[128] = {0};
// 然后用NSData的getBytes:方法拿到數(shù)據(jù)
[characteristic.value getBytes:tmpBuffer];
// 再從中取用數(shù)據(jù)
unsigned char startBit = tmpBuffer[0];
light.brightness = tmpBuffer[5];
light.colorR = tmpBuffer[2];
light.colorG = tmpBuffer[3];
light.colorB = tmpBuffer[4];
……
// 有時(shí)候還要對tmpBuffer操作秀仲,用一堆如memset()融痛、memcpy()等C語言函數(shù),讓對C語言不是特別熟的童鞋直接吐血
上面出現(xiàn)了很多「魔術(shù)數(shù)字」,讓后面看代碼神僵、維護(hù)代碼的人看得云里霧里雁刷,如果復(fù)雜度再高一點(diǎn),直接吐血保礼。
有沒有更好的辦法沛励?我們是這樣做的:
// 專門有一個(gè)類用結(jié)構(gòu)體定義好這些指令
#pragma mark - Device 2 Mobile
#pragma mark Response: 0x13 藍(lán)牙模塊返回?cái)?shù)據(jù)
// 其實(shí)這里有個(gè)坑,當(dāng)單個(gè)數(shù)據(jù)的大小為2字節(jié)或以上時(shí)炮障,我們用UInt16或UInt32去定義目派,會有「自動對齊」的問題,就是接到的數(shù)據(jù)胁赢,沒有按指令定義的順序?qū)R企蹭,導(dǎo)致數(shù)據(jù)不正確,這時(shí)候可以在struct后面加關(guān)鍵字:「__attribute__((packed))」智末。(我掉這個(gè)坑好久谅摄,最后上StackOverflow提問解決)
typedef struct {
UInt8 startBit;
UInt8 cmd;
UInt8 colourR;// 取值范圍:0-255
UInt8 colourG;
UInt8 colourB;
UInt8 brightnessValue;// 取值范圍:0-255, 0為滅,255為最亮
UInt8 reserved;
UInt8 checksum;
} D2MDeviceParamResponse;
// 然后在接收到數(shù)據(jù)的地方,定義并用這個(gè)結(jié)構(gòu)體接收數(shù)據(jù)
const void *raw = characteristics.value.bytes;
D2MDeviceParamResponse *responseData = (D2MDeviceParamResponse *)raw;
// 取用數(shù)據(jù)則這樣
light.brightness = responseData->brightnessValue;
light.colorR = responseData->colourR;
light.colorG = responseData->colourG;
light.colorB = responseData->colourB;
//不會出現(xiàn)一個(gè)「魔術(shù)數(shù)字」吹害,直接看代碼螟凭,就知道是什么東西了虚青。
下面是Swift版本:
// 定義指令
// MARK:- Device 2 Mobile
// MARK:Response: 0x13 藍(lán)牙模塊返回?cái)?shù)據(jù)
struct D2MDeviceParamResponse {
var startBit: UInt8
var cmd: UInt8
var colourR: UInt8
var colourG: UInt8
var colourB: UInt8
var brightnessValue: UInt8
var reserved: UInt8
var checksum: UInt8
}
// 取用數(shù)據(jù)
// 對Swift還不是十分熟悉,不知道還有沒有其他更好的初始化方法(哭)
var cmd = D2MDeviceParamResponse(startBit: 0,
cmd: 0,
colourR: 0,
colourG: 0,
colourB: 0,
brightnessValue: 0,
reserved: 0,
checksum: 0)
characteristic.value!.getBytes(&cmd, length:sizeof(D2MDeviceParamResponse))
light.brightness = cmd.brightnessValue
light.colorR = cmd.colourR
light.colorG = cmd.colourG
light.colorB = cmd.colourB
當(dāng)然它呀,發(fā)送指令也是類似的,先定義好容器(struct),再進(jìn)行賦值封裝發(fā)送纵穿,不再贅述下隧。
這樣是不是會比寫一堆中括號加下標(biāo)索引直觀很多?
大神們說最好的說明文檔就是代碼谓媒,代碼盡量寫得讓人能意會到你的目的淆院、意圖,也算是對代碼的后來維護(hù)者的一大功德~~
好困句惯,睡覺土辩。