iOS - 蓝牙开发(中心模式)
1.CoreBluetooth.
iOS蓝牙的相关操作由CoreBluetooth.framework进行管理。核心主要是两种场景:peripheral和central, 可以理解成外设和中心。
在此主要用到了蓝牙作为中心设备通讯连接硬件的服务。中心模式流程为:
1.建立中心角色;
2.扫描外设 (discover);15:25:21
3.链接外设 (connect);
4.扫描外设种的服务和特征
4.1 获取外设的服务 services;
4.2 获取外设的特征 characteristics;获取特征的descriptor和descriptor的值;
5.与外设做数据交互,读写数据;
6.订阅 characteristics 的通知;
7.断开。
其中:peripheral,central == 外设和中心,发起连接的时central,被连接的设备为perilheral;
service and characteristic === 服务和特征 每个设备会提供服务和特征,类似于服务端的api,但是机构不同。每个外设会有很多服务,每个服务中包含很多字段,这些字段的权限一般分为 读read,写write,通知notiy几种,就是我们连接设备后具体需要操作的内容。
2.代码实现
在实际开发过程中,可以根据需求将蓝牙操作写成单例模式,便于整体规划开发。
首先,硬件说明文档中 一般都会给你 三个UUID类似下面这种:
// 设备service UUID #define BLUETOOTH_SERVICE_UUID @"6E400001-B5A3-F393-E0xxx" // service 下 用来 写数据的 characteristic 的UUID #define BLUETOOTH_CHARACTERISTIC_WRITE_UUID @"6E400002-B5A3-F393-E0xxx" // service 下 用来 读数据的 characteristic 的UUID ,其实这个读数据一般是在写完之后,拿到写入成功硬件的返回通知,所以其实是通知的uuid #define BLUETOOTH_CHARACTERISTIC_READ_UUID @"6E400003-B5A3-F393-E0xxx" //// characteristic 有properite 属性,其中有对应的 读,写,通知 等权限,不同的权限对应不同的功能。
1.建立中心角色
#import <CoreBluetooth/CoreBluetooth.h> @interface BlueToothService (<CBPeripheralDelegate,CBCentralManagerDelegate> //系统蓝牙设备管理对象,可以把他理解为主设备,通过他,可以去扫描和链接外设 @property (nonatomic, strong) CBCentralManager *centerManager; //保存扫描到的设备 @property (nonatomic, strong) NSMutableArray *peripheralArray; //当前链接的设备 @property (nonatomic, strong) CBPeripheral *curPeripheral; - (void)viewDidLoad { [super viewDidLoad]; //初始化并设置委托和线程队列,最好一个线程的参数可以为nil,默认会就main线程 manager = [[CBCentralManager alloc]initWithDelegate:self queue:dispatch_get_main_queue()]; }
2.扫描外设
只有当设备蓝牙打开状态下才会扫描外设,开启扫描之前,需要先判断蓝牙状态,这个在centeralManager成功打开的委托中。
/** * 判断状态 - 开始扫描 */ - (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"); //开始扫描周围的外设 [_centerManager scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @NO}]; break; default: break; } }
其中 1. CBCentralManagerScanOptionAllowDuplicatesKey值为 No,表示不重复扫描已发现的设备;
2.实际扫描时,可能只需要扫描指定设备,第一个nil的参数,可以指定 CBUUID 对象的数组,即services。这时扫描只会返回正在广告这些服务的设备。
扫描到的结果在下面方法中显示:
/** * 扫描结果 */ - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI { NSLog(@"当扫描到设备:%@",peripheral.name); }
1.peripheral:是扫描到的设备,保存在数组中,以备后续链接。
2.advertisementData: 设备的广播信息。一般硬件厂商会在广播信息中广播一些设备信息,如:mac地址等。可以通过硬件文档解相关数据。
3.RSSI: 设备信号强度。这个值是波动的,硬件方应该也会给一个相应的计算方法,确定信号强度的具体程度值。
特别:在解析 advertisementData 广播信息的时候,我遇到了一些问题:
NSData* advData = [dict objectForKey:@"kCBAdvDataManufacturerData"]; 解析出广播信息的data格式,但是这个格式是 NSInlineData,并不是NSData格式的。在各种百度问大佬之后,终于找到了解析 的方法:具体如下:
#pragma mark 将传入的NSData类型转换成NSString并返回 -(NSString *)hexadecimalString:(NSData *)data{ NSString *result; const unsigned char *dataBuffer = (const unsigned char *)[data bytes]; if (!dataBuffer) { return nil; } NSUInteger dataLength = [data length]; NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)]; for (int i = 0; i<dataLength; i++) { //02x 表示两个位置 显示的16进制 [hexString appendString:[NSString stringWithFormat:@"%02lx",(unsigned long)dataBuffer[i]]]; } result = [NSString stringWithString:hexString]; return result; }
3.链接外设
//连接方法,扫描到的peripheral 需要保存下来,且必须持有它 [_centerManager connectPeripheral:peripheral options:nil]; //连接到Peripherals-成功 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { NSLog(@">>>连接到名称为(%@)的设备-成功",peripheral.name); } //连接到Peripherals-失败 -(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { NSLog(@">>>连接到名称为(%@)的设备-失败,原因:%@",[peripheral name],[error localizedDescription]); } //Peripherals断开连接 - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{ NSLog(@">>>外设连接断开连接 %@: %@\n", [peripheral name], [error localizedDescription]); }
4.1扫描外设的服务
//发现服务 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { NSLog(@" service == %@ ---", peripheral.services); for (CBService *service in peripheral.services) { if ([service.UUID isEqual:[CBUUID UUIDWithString:BLUETOOTH_SERVICE_UUID]]) { //查找特征 [self.curPeripheral discoverCharacteristics:nil forService:service]; } } }
4.2.扫描外设的特征
//发现特征 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { NSLog(@"chari == %@ ---", service.characteristics); //write for (CBCharacteristic *character in service.characteristics) { //写 CBCharacteristic if ([character.UUID isEqual:[CBUUID UUIDWithString:BLUETOOTH_CHARACTERISTIC_WRITE_UUID]]) { self.writeCharacteristic = character; } // 通知 if ([character.UUID isEqual:[CBUUID UUIDWithString:BLUETOOTH_CHARACTERISTIC_READ_UUID]]) { self.notifyCharacteristic = character; [self.curPeripheral setNotifyValue:YES forCharacteristic:character]; } } if (_connectedSuccessBlock) { _connectedSuccessBlock(peripheral,self.writeCharacteristic,self.notifyCharacteristic); } }
扫描到特征后,可以根据 上文中 提到的 硬件方 给的对应的uuid 保存获取到相应的 CBCharacteristic;通知属性的CBCharacteristic,需要注册通知,才能拿到返回数据。
5.与外设做数据交互
写数据
//写入数据方法 [self.curPeripheral writeValue:data forCharacteristic:self.writeCharacteristic type:CBCharacteristicWriteWithResponse]; //写成功 返回方法:这个方法 只是表示 此时写入数据成功,但并不是 硬件在写入相应明令之后作出的正确回应,正确返回 是在 通知Characteristic 中返回的 - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { NSLog(@"char == %@ -- ", characteristic); }
//打印出 characteristic 的权限,可以看到有很多种,这是一个NS_OPTIONS,就是可以同时用于好几个值,常见的有read,write,notify,indicate,知知道这几个基本就够用了,前连个是读写权限,后两个都是通知,两种不同的通知方式。 /* typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) { CBCharacteristicPropertyBroadcast = 0x01, CBCharacteristicPropertyRead = 0x02, CBCharacteristicPropertyWriteWithoutResponse = 0x04, CBCharacteristicPropertyWrite = 0x08, CBCharacteristicPropertyNotify = 0x10, CBCharacteristicPropertyIndicate = 0x20, CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40, CBCharacteristicPropertyExtendedProperties = 0x80, CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100, CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200 }; */ NSLog(@"%lu", (unsigned long)characteristic.properties); //只有 characteristic.properties 有write的权限才可以写
其实,如果硬件并没有给出相应的uuid,也是可以 根据characteristic 的属性,判断其是 写 还是通知的 特征。
6.获取通知
一般情况下,成功写入数据之后,比方蓝牙开锁命令,成功输入命令之后,锁会打开,同时,会在 监听的通知属性下characteristic 中,返回 开锁成功等信息。注册通知就是上文 扫描characteristic方法。如下:
[self.curPeripheral setNotifyValue:YES forCharacteristic:character];
监听通知数据返回方法:
//数据更新回掉 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { NSLog(@"char == %@ -- ", characteristic); NSData *value = characteristic.value; }
拿到更新后的value 数据后,根据 硬件文档,解析数据,便可得到正确的返回数据。
7.断开链接
//停止扫描并断开连接 -(void)disconnectPeripheral:(CBCentralManager *)centralManager peripheral:(CBPeripheral *)peripheral{ //停止扫描 [centralManager stopScan]; //断开连接 [centralManager cancelPeripheralConnection:peripheral]; }
3.我遇到的问题
1.首先我做的是蓝牙锁硬件,总体下来感觉第一最重要的:让硬件方给一个尽可能详细的文档!因为这其中不仅包括数据的解析格式,命令格式,uuid等这些,还有可能会有 加密解密算法,具体的初始密钥或者明文秘文规定等。这些都是很重要的。
2.前面提到的在获取广播数据之后 NSInlineData 数据的解析,其实就是将inlinedata转成 char* 字符格式,接着再转换成oc的字符串。
3.byte 数组。蓝牙命令格式 一般是byte数组,我对这些其实并没有怎么接触过。因为我们是要对命令数据 进行加密 之后再写入。在研究了很久之后,才慢慢理清了具体的操作过程。
4,一开始在用babybluetooth,确实是走通了,但是总觉得代码太多,因为需求比较简单。如果需求比较复杂,或者可以直接使用babybluetooth,很全面的第三方蓝牙库。
还有,一开始有两个问题没有清楚导致进度特别慢:没有写数据要用 characteristic 的概念,还有不确定写数据 和 写入成功 后的数据返回在哪。在理清楚写 characteristic,和 通知 characteristic的关系后,整个就变得很容易了。
主要是想对使用过的方法和走过的坑进行一下总结,有意见或建议可以联系我QQ804729713。