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.

相关文章
相关标签/搜索