iOS藍(lán)牙開發(fā)如何更好地收發(fā)數(shù)據(jù)

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)給我們而已扫腺,也就是0xda0x13村象、0xff笆环、0xff0xff厚者、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è)聰明的猿類「定義」的一條指令:

我們將這8個(gè)byte所表示的內(nèi)容定義清楚
  • 第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ù)者的一大功德~~

好困句惯,睡覺土辩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市抢野,隨后出現(xiàn)的幾起案子拷淘,更是在濱河造成了極大的恐慌,老刑警劉巖指孤,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件启涯,死亡現(xiàn)場離奇詭異,居然都是意外死亡恃轩,警方通過查閱死者的電腦和手機(jī)结洼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叉跛,“玉大人松忍,你說我怎么就攤上這事∶粱ィ” “怎么了挽铁?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長敞掘。 經(jīng)常有香客問我叽掘,道長,這世上最難降的妖魔是什么玖雁? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任更扁,我火速辦了婚禮,結(jié)果婚禮上赫冬,老公的妹妹穿的比我還像新娘浓镜。我一直安慰自己,他們只是感情好劲厌,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布膛薛。 她就那樣靜靜地躺著,像睡著了一般补鼻。 火紅的嫁衣襯著肌膚如雪哄啄。 梳的紋絲不亂的頭發(fā)上雅任,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音咨跌,去河邊找鬼沪么。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锌半,可吹牛的內(nèi)容都是我干的禽车。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼刊殉,長吁一口氣:“原來是場噩夢啊……” “哼殉摔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起记焊,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤钦勘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后亚亲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彻采,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年捌归,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肛响。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡惜索,死狀恐怖特笋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情巾兆,我是刑警寧澤猎物,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站角塑,受9級特大地震影響蔫磨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜圃伶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一堤如、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧窒朋,春花似錦搀罢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至欺劳,卻和暖如春唧取,著一層夾襖步出監(jiān)牢的瞬間瓣俯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工兵怯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腔剂。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓媒区,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掸犬。 傳聞我的和親對象是個(gè)殘疾皇子袜漩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容