大致了解了coreBluetooth后,接下来开始着手建立连接的事情。连接大致分为以下几个步骤。
重要提示!!!Xcode10 之后,扫描外设出来的值,会变成
- (instancetype)initWithDelegate:(nullable id)delegate
queue:(nullable dispatch_queue_t)queue
options:(nullable NSDictionary *)options NS_AVAILABLE(10_9, 7_0) NS_DESIGNATED_INITIALIZER;
其中需要注意:options的参数,参数有很多,举例说两个,其他的参考API
/*!
* @const CBCentralManagerOptionShowPowerAlertKey
*
* @discussion An NSNumber (Boolean) indicating that the system should, if Bluetooth is powered off when CBCentralManager
is instantiated, display
* a warning dialog to the user.
*
* @see initWithDelegate:queue:options:
*
*/
CB_EXTERN NSString * const CBCentralManagerOptionShowPowerAlertKey NS_AVAILABLE(10_9, 7_0);
大致意思是说,如果是给的YES,那么在初始化CBCentralManager的时候,如果蓝牙没有打开,那么会以alert的形式提示用户打开蓝牙,但是似乎,iOS11以后就不太好用了。
/*!
* @const CBCentralManagerOptionRestoreIdentifierKey
*
* @discussion An NSString containing a unique identifier (UID) for the CBCentralManager
that is being instantiated. This UID is used
* by the system to identify a specific CBCentralManager
instance for restoration and, therefore, must remain the same for
* subsequent application executions in order for the manager to be restored.
*
* @see initWithDelegate:queue:options:
* @seealso centralManager:willRestoreState:
*
*/
CB_EXTERN NSString * const CBCentralManagerOptionRestoreIdentifierKey NS_AVAILABLE(10_13, 7_0);
个人理解,就是将你初始化的这个CBCentralManager存储到系统蓝牙队列里面,如果你的程序gg以后,当系统蓝牙唤醒你的app以后会通过 centralManager:willRestoreState: 直接将之前的CBCentralManager还给你,而不会再通过init初始化了,所以要注意哦~ 关于这点,后面具体说。willRestore记得设置代理。
其他的请自行查阅文档。
初始化成功以后,不是连接,而是检查系统蓝牙的状态。
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
通过该回调 central.state,获取CBCentralManager的状态也就是系统蓝牙的状态,
typedef NS_ENUM(NSInteger, CBManagerState) {
CBManagerStateUnknown = 0,
CBManagerStateResetting,
CBManagerStateUnsupported,
CBManagerStateUnauthorized,
CBManagerStatePoweredOff,
CBManagerStatePoweredOn,
} NS_ENUM_AVAILABLE(10_13, 10_0);
一共有这几种状态,最后两个是 已关闭、已打开,其他的自行根据api查看。并根据不同的状态触发不同的业务逻辑。
当蓝牙是打开的时候,进行扫描。
- (void)scanForPeripheralsWithServices:(nullable NSArray *)serviceUUIDs options:(nullable NSDictionary *)options;
需要提到的是,如果需要扫描包含某个或几个的指定服务的per时,以CBUUID类型为元素的数组,options 中CBCentralManagerScanOptionSolicitedServiceUUIDsKey, 这样就可以只扫描出包含指定服务的per了。
如果什么都不需要直接填nil 即可。
当有符合条件的per被扫描到后,触发回调
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI;
advertisementData:扫描的per所附带的广播数据,可根据与厂商的协议,获取per的mac值,进行匹配连接
RSSI:蓝牙信号强度
当扫描到匹配的per后,调用连接函数。注意,连接之前,需要把per作为单例的属性保存起来,不然会有不可描述的事情等着你。当然,通过查阅apple的api发现,这个函数,是个很强大的函数,它会将你的连接请求,加入到系统的队列中,如果你的连接丢失,在丢失的回调里,直接再次写入连接就可以了,如果你app在期间gg了,还记得willrestore那个回调吗?
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary *)options;
个人感觉,options里的几个参数没什么意义,直接写nil即可,如果有兴趣,自己也可以测试一下。
建议在此时写一个延时操作,用来校验是否连接成功,如果没有,需要再次连接,或者进行其他逻辑操作。
同时需要把扫描关闭,以减低功耗
- (void)stopScan;
连接成功/失败的回调。成功之后,记得设置代理噢~ per的代理。
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
没啥好说的,自己看着办吧
获取服务与特征。 一般针对per的读写操作,是针对到具体的特征值,所以我们要将自己之后需要操作的特征值,作为单例管理类的对象存储起来。再连接成功的回调里,最好是将per的所以服务特征遍历读一遍。
- (void)discoverServices:(nullable NSArray *)serviceUUIDs;
注意啦,这个是per的对象方法,别用central去调用。参数如果给nil就是遍历,去读取所有的服务。读取到的服务,会触发回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;
是不是发现回调里没有service之类的参数?哈哈,这个时候Peripheral这个对象的属性services已经是有值得了,直接get就可以得到。
接下来就是获取特征值了,根据第一篇里面提到的,特征是属于服务的,所以我们获取特征的时候,需要提供其所属的服务,所以遍历services
- (void)discoverCharacteristics:(nullable NSArray *)characteristicUUIDs forService:(CBService *)service;
类似于获取特征,给nil就是获取该服务的所有特征,该方法触发回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;
呐,其实和获取服务的回调差不多,自己根据厂商的协议,将制定的特征set给单例管理类对应的特征对象的属性即可。
这个回调里,可以根据业务需求,直接先读一下或者订阅下,读或者订阅怎么操作。下面接着讲。。
读。 需要注意读不等同于订阅,两者是有区别的,读,是代码控制去读一下,只是读一下。而 订阅,在开启后,会根据固件方的设定,主动发消息过来,所以,这里也从侧面证明,蓝牙连接,是个长连接。读一次,触发一次回调
//读取指定特征
- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic;
//特征读取回调
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
读取到的特征通过回调中的 characteristic.value获得,所获得的值为NSData类型,下面是解析示例
Byte *byte = (Byte *)[characteristic.value bytes];
NSLog(@"%s", byte);
int sate = byte[0];
NSLog(@"%d", sate);
根据业务需求自行解析
订阅(使能),订阅时需要注意不要重复开启订阅或者关闭订阅。使用场景:动态心跳监测,订阅指定特征后,开启心跳监测,该特征值根据厂商协议,一般会每秒更新一次,具体间隔,由固件决定。app只是被动接收。
- (void)setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic;
同样的,也是通过didUpdateNotificationStateForCharacteristic回调更新特征的值。这里与读的区别,是读一次触发一次,而订阅,是会多次触发,每次收到per发送新的值,就触发一次。
写入。写入,同样是写入NSData对象数据,需要注意的是CBCharacteristicWriteType,要严格按照协议的约定选择CBCharacteristicWriteWithResponse或者CBCharacteristicWriteWithoutResponse。
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
如果type是CBCharacteristicWriteWithResponse,则会触发回调
-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error;
举例一个写入数据
+ (NSData *)ZJjiaozhunDataXinlv:(NSInteger)xinlv Gaoya:(NSInteger)gaoya diya:(NSInteger)diya {
NSData *dda = [[NSData alloc]init];
unsigned char command[4] = {0x82, gaoya, diya, xinlv};
dda = [[NSData alloc] initWithBytes:&command length:4];
return dda;
}
取消连接
- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
连接取消后,会触发回调
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
连接已经断开,注意清空单例类的一些属性值以及对应的代理
意外断开连接需要重连。实际情况中,可能会因为用户关闭蓝牙或者距离过远以及新号不稳定等情况,导致连接中段,这个时候 我们就需要重连了。根据官方文档,在断开连接的回调里直接调用连接函数即可。
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
[central connectPeripheral:peripheral options:nil];
}
关于固件升级,是用iOS-DFULibrary第三方库进行升级,列举下主要函数
//发送固件包到指定蓝牙设备。 这里的url我使用的是本地url,先通过AFNet将固件包下载到本地
- (void)sendFileToWatchWithURL:(NSURL *)url peripheral:(CBPeripheral *)peripheral;
//传输过程中的回调
- (void)dfuProgressDidChangeFor:(NSInteger)part outOf:(NSInteger)totalParts to:(NSInteger)progress currentSpeedBytesPerSecond:(double)currentSpeedBytesPerSecond avgSpeedBytesPerSecond:(double)avgSpeedBytesPerSecond {
// NSLog(@"part: %ld \n outof:%ld \n progress:%ld \n currentSpeedBytesPerSecond:%.2f \n avgSpeedBytesPerSecond:%.2f",part, totalParts, progress,currentSpeedBytesPerSecond,avgSpeedBytesPerSecond);
[SVProgressHUD showProgress:progress/100.0 status:@"固件包传输中"];
}
//传输失败回调
- (void)dfuError:(enum DFUError)error didOccurWithMessage:(NSString *)message;
//整个固件升级的per状态变化
- (void)dfuStateDidChangeTo:(enum DFUState)state {
switch (state) {
case DFUStateConnecting:
NSLog(@"连接:服务正在连接到DFU目标");
break;
case DFUStateStarting:
NSLog(@"启动:DFU服务正在初始化DFU操作");
break;
case DFUStateEnablingDfuMode:
NSLog(@"服务正在将设备切换到DFU模式");
break;
case DFUStateUploading:
NSLog(@"上传:服务正在上传固件");
break;
case DFUStateValidating:
NSLog(@"验证:DFU目标正在验证固件");
break;
case DFUStateDisconnecting:
NSLog(@"disconnecting:iDevice正在断开连接或等待断开连接");
break;
case DFUStateCompleted:
NSLog(@"完成:DFU操作完成并成功");
self.isDFU = NO;
[self.manager cancelPeripheralConnection:self.connectPeripheral];
[self updataSucess];
break;
case DFUStateAborted:
NSLog(@"aborted:DFU操作已中止");
[self updataFail];
break;
default:
break;
}
}