ios 藍牙相關

 

 

ios藍牙開發項目實戰 -(附小米手環實例)

前言

最近一直在開發關於藍牙的功能,原本是不想寫這一篇文章,由於網上關於ios藍牙開發的文章實在太多了,成噸成噸的文章出現,可是很遺憾都只是一些皮毛,或者只是簡單的介紹一下基本概念而已,對於一些小白可能還有不少不少疑惑,因此萌生了寫一篇文章,並附上實際例子的demo,供即將項目中準備開發的夥伴參考。css

正文

首先

咱們得明確一下很重要的幾個概念
1.當前ios中開發藍牙所運用的系統庫是<CoreBluetooth/CoreBluetooth.h>
2.藍牙外設必須爲4.0及以上,不然沒法開發,藍牙4.0設備由於低耗電,因此也叫作BLE。
3.CoreBluetooth框架的核心實際上是兩個東西,peripheral和central, 能夠理解成外設和中心,就是你的蘋果手機就是中心,外部藍牙稱爲外設。
4.服務和特徵(service and characteristic):簡而言之,外部藍牙中它有若干個服務service(服務你能夠理解爲藍牙所擁有的能力),而每一個服務service下擁有若干個特徵characteristic(特徵你能夠理解爲解釋這個服務的屬性)。
5.Descriptor(描述)用來描述characteristic變量的屬性。例如,一個descriptor能夠規定一個可讀的描述,或者一個characteristic變量可接受的範圍,或者一個characteristic變量特定的單位。
6.跟硬件親測,Ios藍牙每次最多接收155字節的數據,安卓5.0如下最大接收20字節,5.0以上能夠更改最大接收量,能達到500多字節。ios

其次

經過以上關鍵信息的解釋,而後看一下藍牙的開發流程:git

  1. 創建中心管理者
  2. 掃描外設(discover)
  3. 鏈接外設(connect)
  4. 掃描外設中的服務和特徵(discover)
    4.1 獲取外設的services
    4.2 獲取外設的Characteristics,獲取Characteristics的值,
    獲取Characteristics的Descriptor和Descriptor的值
  5. 與外設作數據交互(explore and interact)
  6. 斷開鏈接(disconnect)

具體代碼github

1.建立一箇中心管理者

//只要一觸發這句代碼系統會自動檢測手機藍牙狀態,你必須實現其代理方法,固然得添加<CBCentralManagerDelegate> CBCentralManager *theManager = [[CBCentralManager alloc]initWithDelegate:self queue:nil]; //從這個代理方法中你能夠看到全部的狀態,其實咱們須要的只有on和off連個狀態 - (void)centralManagerDidUpdateState:(CBCentralManager *)central{ switch (central.state) { case CBCentralManagerStateUnknown: NSLog(@">>>CBCentralManagerStateUnknown"); break; case CBCentralManagerStateResetting: NSLog(@">>>CBCentralManagerStateResetting"); break; case CBCentralManagerStateUnsupported: NSLog(@">>>CBCentralManagerStateUnsupported"); break; case CBCentralManagerStateUnauthorized: NSLog(@">>>CBCentralManagerStateUnauthorized"); break; case CBCentralManagerStatePoweredOff: NSLog(@">>>CBCentralManagerStatePoweredOff"); break; case CBCentralManagerStatePoweredOn: NSLog(@">>>CBCentralManagerStatePoweredOn"); break; default: break; }

當發現藍牙狀態是開啓狀態,你就能夠利用中央設備進行掃描外設,若是爲關閉狀態,系統會自動彈出讓用戶去設置藍牙,這個不須要咱們開發者關心。算法

2.利用中心去掃描外設

//兩個參數爲nil,默認掃描全部的外設,能夠設置一些服務,進行過濾搜索 [theManager scanForPeripheralsWithServices:nil options:nil];

2.1當掃描到外設,觸發如下代理方法

在這裏須要說明的是,
一.當掃描到外設,咱們能夠讀到相應外設廣播信息,RSSI信號強度(能夠利用RSSI計算中心和外設的距離)。
二.咱們能夠根據必定的規則進行鏈接,通常是默認名字或者名字和信號強度的規則來鏈接。
三.像我如今作的無鑰匙啓動車輛鎖定車輛,就須要加密通信,不能誰來鏈接均可以操做車輛,須要和外設進行加密通信,可是一切的經過算法的校驗都是在和外設鏈接上的基礎上進行,例如鏈接上了,你發送一種和硬件約定好的算法數據,硬件接收到校驗經過了就正常操做,沒法經過則由硬件(外設)主動斷開。api

//這裏默認掃到MI,主動鏈接,固然也能夠手動觸發鏈接 - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{ NSLog(@"掃描鏈接外設:%@ %@",peripheral.name,RSSI); if ([peripheral.name hasSuffix:@"MI"]) { //保存外設,並中止掃描,達到節電效果 thePerpher = peripheral; [central stopScan]; //進行鏈接 [central connectPeripheral:peripheral options:nil]; } }

3.當鏈接到外設,會調用如下代理方法

這裏須要說明的是
當成功鏈接到外設,須要設置外設的代理,爲了掃描服務調用相應代理方法數組

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{ NSLog(@"鏈接外設成功!%@",peripheral.name); [peripheral setDelegate:self]; [peripheral discoverServices:nil]; } //鏈接外設失敗 -(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { NSLog(@"鏈接到外設 失敗!%@ %@",[peripheral name],[error localizedDescription]); }

4.掃描外設中的服務和特徵

//掃描到服務 -(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{ if (error) { NSLog(@"掃描外設服務出錯:%@-> %@", peripheral.name, [error localizedDescription]); return; } NSLog(@"掃描到外設服務:%@ -> %@",peripheral.name,peripheral.services); for (CBService *service in peripheral.services) { [peripheral discoverCharacteristics:nil forService:service]; } NSLog(@"開始掃描外設服務的特徵 %@...",peripheral.name); } //掃描到特徵 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{ if (error) { NSLog(@"掃描外設的特徵失敗!%@->%@-> %@",peripheral.name,service.UUID, [error localizedDescription]); return; } NSLog(@"掃描到外設服務特徵有:%@->%@->%@",peripheral.name,service.UUID,service.characteristics); //獲取Characteristic的值 for (CBCharacteristic *characteristic in service.characteristics){ //這裏外設須要訂閱特徵的通知,不然沒法收到外設發送過來的數據 [peripheral setNotifyValue:YES forCharacteristic:characteristic]; //這裏以小米手環爲例,當咱們定義好每一個特徵是幹什麼用的,咱們須要讀取這個特徵的值,當特徵值更新了會調用 //- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error方法 //須要說明的是UUID是硬件定義好給你,若是硬件也是個新手,那你能夠先打印出全部的UUID,找出有用的 //步數 if ([characteristic.UUID.UUIDString isEqualToString:@"FF06"]) { [peripheral readValueForCharacteristic:characteristic]; } //電池電量 else if ([characteristic.UUID.UUIDString isEqualToString:@"FF0C"]) { [peripheral readValueForCharacteristic:characteristic]; } else if ([characteristic.UUID.UUIDString isEqualToString:@"2A06"]) { //震動 theSakeCC = characteristic; } } } } //掃描到具體的值->通信主要的獲取數據的方法 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error { if (error) { NSLog(@"掃描外設的特徵失敗!%@-> %@",peripheral.name, [error localizedDescription]); return; } NSLog(@"%@ %@",characteristic.UUID.UUIDString,characteristic.value); if ([characteristic.UUID.UUIDString isEqualToString:@"FF06"]) { Byte *steBytes = (Byte *)characteristic.value.bytes; int steps = bytesValueToInt(steBytes); } else if ([characteristic.UUID.UUIDString isEqualToString: @"FF0C"]) { Byte *bufferBytes = (Byte *)characteristic.value.bytes; int buterys = bytesValueToInt(bufferBytes)&0xff; NSLog(@"電池:%d%%",buterys); } else if ([characteristic.UUID.UUIDString isEqualToString:@"2A06"]) { Byte *infoByts = (Byte *)characteristic.value.bytes; //這裏解析infoByts獲得設備信息 } }

5.與外設作數據交互

須要說明的是蘋果官方提供發送數據的方法很簡單,只須要調用下面的方法bash

- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type

咱們只須要在搜索每一個服務的特徵,記錄這個特徵,而後向這個特徵發送數據就能夠了。app

6.斷開鏈接

調用如下代碼,須要說明的是中心斷開與外設的鏈接。框架

- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;

以上呢是整個藍牙的開發過程,系統提供的框架api就這麼多,開發起來也不是很難,要是你認爲這篇文章到這裏就結束了,你就大錯特錯了,這篇文章的精華內容將從這裏開始,因爲公司項目的保密性,我不能以它爲例,那我就以小米手環爲實例,主要分享一下數據解析。

精華部分

1.當調用瞭如下代理方法的時候,咱們須要處理接收到的數據

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error

小米手環所定義的幾個UUID以下:

@"FF06" 這個UUID定義的是步數
@"FF0C" 這個UUID定義的是電量
@"2A06"這個UUID定義的是震動
@"FF01"這個UUID定義的是相關的設備信息

經過以上的UUID,咱們能夠讀取到步數,電量,操做手環震動,並讀取手環相應設備的信息,這裏須要說明的是我並不清楚設備信息的具體協議,因此這裏無法解析。

if ([characteristic.UUID.UUIDString isEqualToString:STEP]) { Byte *steBytes = (Byte *)characteristic.value.bytes; int steps = bytesValueToInt(steBytes); NSLog(@"步數:%d",steps); }

當咱們讀到步數這個UUID時,咱們能夠拿到value,小米手環所定義的協議是4個字節,咱們須要將4個字節轉換爲int 類型便可
方法以下

//4個字節Bytes 轉 int unsigned int bytesValueToInt(Byte *bytesValue) { unsigned int intV; intV = (unsigned int ) ( ((bytesValue[3] & 0xff)<<24) |((bytesValue[2] & 0xff)<<16) |((bytesValue[1] & 0xff)<<8) |(bytesValue[0] & 0xff)); return intV; }

須要說明的是這個方法是C語言的方法,採用位與運算,固然若是項目中須要另外一種方式的轉換,如:發過來兩字節須要你轉換爲int,若是你不會轉換,能夠去網上搜索,我會在文章後附一些經常使用的轉換方法。
這裏重點說明的是步數讀取,剩餘相似。

2.當咱們給外設發送數據時,咱們須要跟硬件定協議,固然這是在開始項目以前作好的事情。

小米手環協議中震動命令的觸發,是向硬件發送一個10進制的 2
這裏須要說明的是咱們發送數據給硬件通常是字節數組,而後將他轉換爲NSData發送。

//這裏爲了嚴謹起見,須要判斷外設和特徵是否存在,若是存在發送數據 if (thePerpher && theSakeCC) { Byte zd[1] = {2}; NSData *theData = [NSData dataWithBytes:zd length:1]; [thePerpher writeValue:theData forCharacteristic:theSakeCC type:CBCharacteristicWriteWithoutResponse]; }

這裏須要再添加一點,若是協議要求你發ASCII碼,例如‘SHAKE’,只須要這麼處理

if (thePerpher && theSakeCC) { Byte zd[] = {'S','H','A','K','E'}; NSData *theData = [NSData dataWithBytes:zd length:1]; [thePerpher writeValue:theData forCharacteristic:theSakeCC type:CBCharacteristicWriteWithoutResponse]; }

3.項目中實際開發的運用
當咱們面對實際開發時,咱們不可能這麼直接去在一個控制器中去寫這麼多代碼,若是你說這沒多少啊,那我無話可說了😄。。。固然有人會說運用三方庫的啊,babyBluetooth在github上star仍是挺高的,個人觀點是沒有必要去依賴所謂的三方庫,有的三方庫高度封裝性會導致咱們若是遇到錯誤時沒法排查,因此三方庫慎用,固然你能夠參考一些Star很高的三方庫,看大神的代碼思想,有利於本身讀寫代碼的能力。

個人主要思路是封裝一個單例類,封裝好掃描的方法,讀取數據的方法(通常是代理回調),發送指令(例如小米的震動)方法,而在讀取數據中咱們能夠採用模型的思想,當收到藍牙外設發送過來的數據時候,咱們解析完後包裝成模型經過代理回傳過去,之後咱們在控制器中每次拿到的都是模型數據,這樣處理起來方便大大的。
下面將小米手環demo附上,供須要的朋友參考學習,若是文章中我有什麼沒有說的很明白,或者什麼疑惑能夠留言。

demo https://github.com/markdashi/MIBLE

附經常使用轉換方法

@interface NSString (Extension) //16進制字符串轉成int - (int)convertHexStringToINT; //16進制字符串轉換爲2進制的字符串 - (NSString *)getBinaryByhex; @end @implementation NSString (Extension) //不考慮內存溢出 - (int)convertHexStringToINT { UInt64 mac1 = strtoul([self UTF8String], 0, 16); return [[NSString stringWithFormat:@"%llu",mac1] intValue]; } - (NSString *)getBinaryByhex { NSMutableDictionary *hexDic = [[NSMutableDictionary alloc] init]; hexDic = [[NSMutableDictionary alloc] initWithCapacity:16]; [hexDic setObject:@"0000" forKey:@"0"]; [hexDic setObject:@"0001" forKey:@"1"]; [hexDic setObject:@"0010" forKey:@"2"]; [hexDic setObject:@"0011" forKey:@"3"]; [hexDic setObject:@"0100" forKey:@"4"]; [hexDic setObject:@"0101" forKey:@"5"]; [hexDic setObject:@"0110" forKey:@"6"]; [hexDic setObject:@"0111" forKey:@"7"]; [hexDic setObject:@"1000" forKey:@"8"]; [hexDic setObject:@"1001" forKey:@"9"]; [hexDic setObject:@"1010" forKey:@"A"]; [hexDic setObject:@"1011" forKey:@"B"]; [hexDic setObject:@"1100" forKey:@"C"]; [hexDic setObject:@"1101" forKey:@"D"]; [hexDic setObject:@"1110" forKey:@"E"]; [hexDic setObject:@"1111" forKey:@"F"]; NSMutableString *binaryString=[[NSMutableString alloc] init]; for (int i=0; i<[self length]; i++) { NSRange rage; rage.length = 1; rage.location = i; NSString *key = [self substringWithRange:rage]; binaryString = (NSMutableString *)[NSString stringWithFormat:@"%@%@",binaryString,[NSString stringWithFormat:@"%@",[hexDic objectForKey:key]]]; } return binaryString; } //NSData轉換爲16進制字符串,NSData的分類 - (NSString *)dataToHexString { NSUInteger len = [self length]; char * chars = (char *)[self bytes]; NSMutableString * hexString = [[NSMutableString alloc] init]; for(NSUInteger i = 0; i < len; i++ ) [hexString appendString:[NSString stringWithFormat:@"%0.2hhx", chars[i]]]; return hexString; }

2016.8.29補充

因爲項目中須要作關於後臺持續掃描,相似於常見的藍牙音箱,打開手機APP鏈接藍牙音箱,播放音樂,當手機遠離藍牙音箱後,中止播放,當手機靠近的時候,藍牙音箱又開始播放了,對於這中需求的實現我開始很困惑,藍牙如何後臺持續掃描呢,我嘗試了不少方法是失敗的,通過我多方面查詢資料弄清楚如何實現這個需求:

1.須要後臺運行需申請後臺權限



勾選便可擁有後臺權限,若是外設持續發送數據,APP端能夠接收到數據。

2.掃描時需指定serviceUUID,需外設廣播出本身的SeviceUUID,APP端做爲掃描的條件。

這是蘋果掃描方法的的官方解釋:

Applications that have specified the bluetooth-central background mode are allowed to scan while backgrounded, with two

  • caveats: the scan must specify one or more service types in serviceUUIDs, and the CBCentralManagerScanOptionAllowDuplicatesKey
  • scan option will be ignored.

顯而易見的說的很清楚,後臺模式時藍牙做爲central,必須指定serviceUUIDs,scan option忽略。


例子

掃描方法:

[self.centralManger scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:@"FEE0"],[CBUUID UUIDWithString:@"FEE7"]] options:nil];

這樣當在後臺的時候是能夠持續掃描的。

3.當後臺斷開鏈接時候會調用系統方法

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error

咱們須要在這裏設置自動重連,便可實現上述的需求。

2016.10.19補充

關於藍牙須要作一些補充及修改,首先對於後臺掃描時候,咱們申請後臺權限只須要申請


權限申請

若是兩個都申請,若是提交安裝包到APPStore,你會被拒絕

If your app is meant to work with external hardware, supported protocols must be included in the UISupportedExternalAccessoryProtocols key in your app's Info.plist file - and the hardware's PPID # should be provided in the Review Notes field of your app in iTunes Connect.Additionally, your app must be authorized by MFi to use the desired hardware. If you are not yet in the MFi Program, you can enroll at MFi program.Please either revise your Info.plist to include the UISupportedExternalAccessoryProtocols key and update your Review Notes to include the PPID # - or remove the external-accessory value from the UIBackgroundModes key.

相關文章
相關標籤/搜索