一、技术背景
本文主要是从蓝牙的扫描、连接、收发数据、打印等方向快速熟悉蓝牙开发,记录了在开发过程中遇到的的问题及解决方法。在分享之前,我们需要清楚几个BLE相关的概念。
二、基本概念
蓝牙,指的是BLE(Bluetooth Low Energy/低功耗蓝牙),一般应用苹果的官方框架基于<CoreBluetooth/CoreBluetooth.h>框架进行开发。
中心设备:用于扫描周边蓝牙外设的设备,比如我们上面所说的中心者模式,此时我们的手机就是中心设备。
外设:被扫描的蓝牙设备,比如我们上面所说的用我们的手机连接小米手环,这时候小米手环就是外设。
广播:外部设备不停的散播的蓝牙信号,让中心设备可以扫描到,也是我们开发中接收数据的入口。
服务(Service):外部设备在与中心设备连接后会有服务,可以理解成一个功能模块,中心设备可以读取服务,筛选我们想要的服务,并从中获取出我们想要特征。(外设可以有多个服务)
特征(Characteristic):服务中的一个单位,一个服务可以多个特征,而特征会有一个value,一般我们向蓝牙设备写入数据、从蓝牙设备读取数据就是这个value
UUID:区分不同服务和特征的唯一标识,使用该字端我们可以获取我们想要的服务或者特征
核心类:CBCentralManager 中心设备管理类、CBCentral 中心设备、CBPeripheralManager 外设设备管理类、CBPeripheral 外设设备、CBUUID 外围设备服务特征的唯一标志、CBService 外围设备的服务、CBCharacteristic 外围设备的特征。
三、申请权限
1、需要在info.plist文件中添加相对应的键值对Privacy - Bluetooth Always Usage Description,否则会闪退。
四、核心重点:蓝牙数据接收的一般流程
1、蓝牙开启后,不断地在进行广播信号
2、扫描蓝牙
3、发现(discover)外设设备(可根据service的UUID来辨别是否是我们连接的设备)
4、成功连接外设设备
5、调用代理方法发现「服务」
6、调用代理方法发现「服务」里的「特征」
7、发现硬件用于传输数据的「特征」(App发送数据给硬件时,会用到这个「特征)
8、发现硬件用于数据输出的「特征」,进行「监听」(硬件就是从这个「特征」中发送数据给手机端)
9、利用数据输入「特征」发送数据,或者等待数据输出「特征」发出来的数据
五、中心设备-相关函数
1、创建一个中心设备
- (instancetype)init;
- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
queue:(nullable dispatch_queue_t)queue;
- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
queue:(nullable dispatch_queue_t)queue
options:(nullable NSDictionary<NSString *, id> *)options NS_AVAILABLE(10_9, 7_0) NS_DESIGNATED_INITIALIZER;
2、中心设备是否正在扫描
@property(nonatomic, assign, readonly) BOOL isScanning NS_AVAILABLE(10_13, 9_0);
3、获取已配对过的蓝牙外设
- (NSArray *)retrievePeripheralsWithIdentifiers:(NSArray *)identifiers NS_AVAILABLE(10_9, 7_0);- (NSArray *)retrieveConnectedPeripheralsWithServices:(NSArray *)serviceUUIDs NS_AVAILABLE(10_9, 7_0);
4、扫描外设(如果参数传nil,表示扫描所有外设)和停止扫描
- (void)scanForPeripheralsWithServices:(nullable NSArray *)serviceUUIDs options:(nullable NSDictionary *)options;- (void)stopScan;
5、连接指定外设
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary *)options;
6、取消指定外设
- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
7、监听中心设备的状态
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
主要是获取当前中心外设状态:
typedef NS_ENUM(NSInteger, CBManagerState) {
CBManagerStateUnknown = 0, // 未知外设类型
CBManagerStateResetting, // 正在重置蓝牙外设
CBManagerStateUnsupported,
CBManagerStateUnauthorized,
CBManagerStatePoweredOff,
CBManagerStatePoweredOn,
} NS_ENUM_AVAILABLE(10_13, 10_0);
8、扫描到外设就会调用一次的代理方法
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI;
9、成功连接指定外设的代理回调
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
10、连接失败后的代理回调
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
11、连接外设失败后的代理回调
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
六、外设设备-相关函数
1、外设名称
@property(retain, readonly, nullable) NSString *name;
2、外设信号强度
@property(retain, readonly, nullable) NSNumber *RSSI NS_DEPRECATED(10_7, 10_13, 5_0, 8_0);
3、外设设备连接状态
@property(readonly) CBPeripheralState state;
typedef NS_ENUM(NSInteger, CBPeripheralState) {
CBPeripheralStateDisconnected = 0, // 断开连接状态
CBPeripheralStateConnecting, // 正在连接状态
CBPeripheralStateConnected, // 已连接状态
CBPeripheralStateDisconnecting NS_AVAILABLE(10_13, 9_0), // 正在断开状态
} NS_AVAILABLE(10_9, 7_0);
4、获取外设服务
@property(retain, readonly, nullable) NSArray *services;
5、发现服务
- (void)discoverServices:(nullable NSArray *)serviceUUIDs;
6、发现子服务
- (void)discoverIncludedServices:(nullable NSArray *)includedServiceUUIDs forService:(CBService *)service;
7、发现特征
- (void)discoverCharacteristics:(nullable NSArray *)characteristicUUIDs forService:(CBService *)service;
8、蓝牙发送数据有字节长度大小限制,该函数是获取允许最大字节长度限制
- (NSUInteger)maximumWriteValueLengthForType:(CBCharacteristicWriteType)type NS_AVAILABLE(10_12, 9_0);
9、发送数据
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
10、发现特征的描述
- (void)discoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic;
11、 发送数据通过描述
- (void)writeValue:(NSData *)data forDescriptor:(CBDescriptor *)descriptor;
12、外设名称改变的监听
- (void)peripheralDidUpdateName:(CBPeripheral *)peripheral NS_AVAILABLE(10_9, 6_0);
13、 服务修改的监听
- (void)peripheral:(CBPeripheral *)peripheral didModifyServices:(NSArray *)invalidatedServices NS_AVAILABLE(10_9, 7_0);
14、信号强度改变的监听
- (void)peripheralDidUpdateRSSI:(CBPeripheral *)peripheral error:(nullable NSError *)error NS_DEPRECATED(10_7, 10_13, 5_0, 8_0);- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(nullable NSError *)error NS_AVAILABLE(10_13, 8_0);
15、 发现服务和子服务
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;- (void)peripheral:(CBPeripheral *)peripheral didDiscoverIncludedServicesForService:(CBService *)service error:(nullable NSError *)error;
16、通过服务获取特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;
17、特征发生改变后的监听
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
18、数据发送结果的回调
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
19、发现描述通过特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
19、描述值发生改变的监听
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(nullable NSError *)error;
120、发送描述结果的回调
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForDescriptor:(CBDescriptor *)descriptor error:(nullable NSError *)error;
七、扫描外设设备和停止扫描
1、检测中心设备的蓝牙状态
// 扫描可用蓝牙外设
- (void)fs_scanPeripheralsSuccess:(FSScanPerpheralsSuccess)success
failure:(FSScanPeripheralFailure)failure {
_scanPerpheralSuccess = success;
_scanPerpheralFailure = failure;
NSString *msg = nil;
// 在扫描设备前,需要判断当前中心设备的蓝牙状态,只有开启后才能进行扫描工作
switch (_centralManager.state) {
case CBManagerStatePoweredOn:{
msg = @"蓝牙已开启,允许连接蓝牙外设";
// 扫描的核心方法
[_centralManager scanForPeripheralsWithServices:nil options:nil];
FSLog(@"扫描阶段 -- %@",msg);
return;
}
break;
case CBManagerStatePoweredOff:{
msg = @"蓝牙是关闭状态,需要打开才能连接蓝牙外设";
}
break;
case CBManagerStateUnauthorized: {
msg = @"蓝牙权限未授权";
}
break;
case CBManagerStateUnsupported:{
msg = @"平台不支持蓝牙";
}
break;
case CBManagerStateUnknown: {
msg = @"未知状态";
}
break;
default:
break;
}
[self initBluetoothConfig];
FSLog(@"%@",msg);
}
// 停止扫描
- (void)fs_stopScan {
[_centralManager stopScan];
}
2、 代理方法获取中心设备蓝牙状态的回调
#pragma mark - CBCentralManagerDelegate - 中央设备的代理方法
// 获取当前中央设备的蓝牙状态,如果蓝牙不可用,这回调回去,若蓝牙可用,则搜索设备
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
if(central.state != CBManagerStatePoweredOn) {
if(_scanPerpheralFailure) {
_scanPerpheralFailure(central.state);
}
}else {
[central scanForPeripheralsWithServices:nil options:nil];
}
FSLog(@"中央设备的蓝牙状态: %ld", central.state);
}
3、 获取扫描到的外设设备: 需要做几个核心操作:
3.1、筛选出peripheral为nil的外设信息
3.2、根据唯一的标识UUID,避免相同外设重复添加到集合中
3.3、自动重连:记录上一次连接的外设UUID,然后通过UUID获取peripheral进行重连
// 扫描蓝牙设备
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
// 在扫描的过程中会有很多不可用的蓝牙设备信息,name为nil,需要排除掉
if(peripheral.name.length <= 0 || peripheral == nil) {
return;
}
// 在扫描过程中,存在同一台设备被多次扫描到,所以在添加到可用设备集合中需要进行筛选,相同的设备不需要重复添加
if(_peripherals.count == 0) {
[_peripherals addObject:peripheral];
[_rssis addObject:RSSI];
} else {
__block BOOL isExist = NO; // block中获取外部变量,若要改值,需要__block处理
// UUIDString是每台设备的唯一标识,所以通过UUIDString查询集合中是否已存在蓝牙外设
[_peripherals enumerateObjectsUsingBlock:^(CBPeripheral * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
CBPeripheral *per = [_peripherals objectAtIndex:idx];
if ([per.identifier.UUIDString isEqualToString:peripheral.identifier.UUIDString]) {
isExist = YES;
[_peripherals replaceObjectAtIndex:idx withObject:peripheral];
[_rssis replaceObjectAtIndex:idx withObject:RSSI];
}
}];
// 集合中不存在,则添加,存在如上则代替
if (!isExist) {
[_peripherals addObject:peripheral];
[_rssis addObject:RSSI];
}
}
// 来这里说明成功扫描到蓝牙设备,回调出去
if(_scanPerpheralSuccess){
_scanPerpheralSuccess(_peripherals, _rssis);
}
// 自动连接上一次连接的外设
if (_isAutoConnect) {
NSString *uuid = [self fs_previousConnectionPeripheralUUID];
if ([peripheral.identifier.UUIDString isEqualToString:uuid]) {
peripheral.delegate = self;
[_centralManager connectPeripheral:peripheral options:nil];
}
}
FSLog(@"扫描到的外设名称: %@", peripheral.name);
}
八、连接外设
1、在连接外设前需要判断是否正在连接有其他外设,如果有需要先取消连接后再重新连接外设,需要注意的是,取消连接时需要清除保存的此时连接的外设UUID,以及保存在集合可打印的数据。
// 连接指定蓝牙设备
- (void)fs_connectPeripheral:(CBPeripheral *)peripheral
completion:(FSConnectPeripheralCompletion)completion {
_connectCompletion = completion;
if(_connectedPerpheral) { // 如果正在连接的有蓝牙外设,需要先取消连接后再连接新的蓝牙设备
[self fs_canclePeripheralConnected:peripheral];
}
[self connectPeripheral:peripheral];
// 连接超时的相关处理
// TODO: ......
}
// 连接蓝牙设备
- (void)connectPeripheral:(CBPeripheral *)peripheral{
[_centralManager connectPeripheral:peripheral options:nil];
peripheral.delegate = self;
}
// 自动连接
- (void)fs_autoConnectPreviousPeripheral:(FSConnectPeripheralCompletion)completion {
_connectCompletion = completion;
_isAutoConnect = YES;
if (_centralManager.state == CBManagerStatePoweredOn) {
// 扫描外设
[_centralManager scanForPeripheralsWithServices:nil options:nil];
}
}
// 取消蓝牙连接
- (void)fs_canclePeripheralConnected:(CBPeripheral *)peripheral {
if (!peripheral) return;
// 取消后需要清除保存的蓝牙外设的uuid
[self fs_removePreviousConnectionPeripheralUUID];
[_centralManager cancelPeripheralConnection:peripheral];
_connectedPerpheral = nil;
// 既然取消了连接,那么就不能发送数据, 所以需要将发送数据的数组清除掉
[_writeChatacterDatas removeAllObjects];
}
2、外设设备管理类连接的代理方法
// 蓝牙外设连接成功后的代理回调
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
// 蓝牙设备
_connectedPerpheral = peripheral;
// 连接成功后停止扫描
[_centralManager stopScan];
// 保存当前蓝牙外设,便于下次自动连接
[self fs_savePreviousConnectionPeripheralUUID:peripheral.identifier.UUIDString];
// 成功连接后的结果回调出去
if(_connectCompletion) {
_connectCompletion(peripheral, nil);
}
// 处于连接状态
_state = kFSBLEStageConnection;
// 外设代理
peripheral.delegate = self;
// 发现服务
[peripheral discoverServices:nil];
FSLog(@"成功连接蓝牙外设: %@", peripheral.identifier.UUIDString);
}
// 连接失败后的回调
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error{
if (_connectCompletion) {
_connectCompletion(peripheral,error);
}
_state = kFSBLEStageConnection;
FSLog(@"连接蓝牙外设失败Error: %@", error);
}
// 断开蓝牙连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
_connectedPerpheral = nil;
[_writeChatacterDatas removeAllObjects];
if (_disConnectCompletion) {
_disConnectCompletion(peripheral,error);
}
_state = kFSBLEStageConnection;
FSLog(@"断开蓝牙外设连接:%@ -- %@", peripheral, error);
}
九、发现服务和特征
1、连接成功后会调用外设代理方法,通过下列几个函数发现服务和特征
#pragma mark - CBPeripheralDelegate - 外设的代理方法
// 发现服务的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
if(error) {
FSLog(@"发现服务错误: %@", error);
return;
}
FSLog(@"发现服务数组:%@",peripheral.services);
for (CBService *service in peripheral.services) {
[peripheral discoverCharacteristics:nil forService:service];
}
_state = kFSBLEStageSeekServices;
}
// 发现特性
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error{
if (error) {
FSLog(@"发现特性出错 错误原因: %@",error.domain);
}else{
for (CBCharacteristic *character in service.characteristics) {
CBCharacteristicProperties properties = character.properties;
if (properties & CBCharacteristicPropertyWrite) {
NSDictionary *dict = @{@"character":character,@"type":@(CBCharacteristicWriteWithResponse)};
[_writeChatacterDatas addObject:dict];
}
}
}
if (_writeChatacterDatas.count > 0) {
_state = kFSBLEStageSeekCharacteristics;
}
}
十、写数据操作
1、发送数据有两种情况:1,当发送的数据小于蓝牙支持的最大长度,直接发送即可。2,如果发送的数据长度大于蓝牙支持最大长度, 需要进行分包发送,每段长度设置成当前蓝牙支持的指定长度,若有剩余,则直接发送即可。
// 发送数据
- (void)fs_writeData:(NSData *)data completion:(FSWriteCompletion)completion {
if (!_connectedPerpheral) {
if (completion) {
completion(NO,_connectedPerpheral,@"蓝牙设备未连接");
}
return;
}
if (self.writeChatacterDatas.count == 0) {
if (completion) {
completion(NO,_connectedPerpheral,@"该蓝牙设备不支持发送数据");
}
return;
}
NSDictionary *dict = [_writeChatacterDatas lastObject];
_writeCount = 0;
_responseCount = 0;
if (_limitLength <= 0) {
_results = completion;
[_connectedPerpheral writeValue:data forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
_writeCount ++;
return;
}
if (data.length <= _limitLength) {
_results = completion;
[_connectedPerpheral writeValue:data forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
_writeCount ++;
} else {
// 分段发送
NSInteger index = 0;
for (index = 0; index < data.length - _limitLength; index += _limitLength) {
NSData *subData = [data subdataWithRange:NSMakeRange(index, _limitLength)];
[_connectedPerpheral writeValue:subData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
_writeCount++;
}
_results = completion;
NSData *leftData = [data subdataWithRange:NSMakeRange(index, data.length - index)];
if (leftData) {
[_connectedPerpheral writeValue:leftData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
_writeCount++;
}
}
}
2、数据发送之后结果的回调也是外设管理类的代理函数
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (!_results) {
return;
}
_responseCount ++;
if (_writeCount != _responseCount) {
return;
}
if (error) {
FSLog(@"发送数据失败: %@",error);
_results(NO,_connectedPerpheral,@"数据发送失败");
} else {
_results(YES,_connectedPerpheral,@"数据已成功发送至蓝牙设备");
}
}
十一、开发过程中遇到的问题
问题1:直接调用- (void)fs_scanPeripheralsSuccess:(FSScanPerpheralsSuccess)success failure:(FSScanPeripheralFailure)failure函数时,搜索不到设备的问题,返回的nil。
答:当首次调用函数搜索设备外设时,无法获取外设设备信息的原因是central的state为CBCentralManagerStateUnknown,这个状态表示手机设备的蓝牙状态为未开启。解决方法:需要在此委托方法中监听蓝牙状态的状态改变为ON时,去开启扫描操作(具体看外设蓝牙状态代理方法)。
问题2:外设蓝牙名称被修改后可能搜索不到的问题
答: 在测试的过程中正常获取蓝牙名称是通过peripheral.name获取,但是可能存在这种情况是当修改连接过的蓝牙名称后,可能存在搜索不到的情况。解决方法:在蓝牙的广播数据中 根据@"kCBAdvDataLocalName"这个key便可获得准确的蓝牙名称。
问题3:调用断开蓝牙的接口,手机蓝牙并没有马上与外设断开连接,而是等待5秒左右的时间后才真正断开。
答:解决方法:可以与硬件开发的同事沟通,从设备收到数据后主动断开连接即可。
问题4:是否能长时间处于后台
答:可以,后台长时间执行需要开启Background Modes,并勾选如图选项。
Background Modes
可以在App启动的方法中可以检测后台是蓝牙的处理情况如图:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UIDevice *device = [UIDevice currentDevice]; BOOL backgroundSupported = NO; if([device respondsToSelector:@selector(isMultitaskingSupported)]) { backgroundSupported = YES; } if (backgroundSupported) { __block int index = 0; NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { // 执行蓝牙相关操作 NSLog(@"[SDK - Background] - %d", index++); // 检测后台是蓝牙的执行情况 }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [timer fire]; // 用了fire方法之后会立即执行定时器的方法 } return YES;}
问题5:蓝牙允许连接的最大距离支持是多少
答: iOS 蓝牙允许连接的最大距离的限制是10m。
问题6:蓝牙连接成功需要多长时间
答:正常连接周围的蓝牙外设一般时在5秒内,如图下:
连接成功的时间差
连接成功的时间差
问题7:蓝牙成功发送数据需要多少时间
答:下图的发送数据时一张图
发送的是图片
问题8:多台设备是否能同时连接
答:官方文档,以及蓝牙底层协议,说明理论上可以支持到同时连接 7 个,但这 7 个能同时正常工作么?貌似不能(三个蓝牙耳机测试的结果),毕竟对于 iOS 而言,蓝牙也是一种资源,同时连接和同时使用消耗,占用的资源肯定不同,而且不同手机,性能也不同。
实现思路:
一个中心设备CBCentralManager,连接多个外设设备CBPeripheral,创建一个中心设备,需要连接何种设备,就单独去连接即可(换句话说就是多次实现单连接)。
for(Model *model in _peripheralMarr) {CBPeripheral *perip = mo.peripheral;[self.centralManager connectPeripheral:perip options:nil];perip.delegate =self;[self.perip discoverServices:nil];}
1、将成功添加的外设CBPeripheral添加到外设数组中(连接成功后处理)
2、 每个外设设备都对应一个唯一的peripheral.identifier或ServiceUUID,所以可以利用他们获取到之前连接的外设数组,根据这个标识,匹配到对应的设备和实现重连机制。
3、若手动断开外设连接,需要将之从外设数组中移除掉。
问题9:如何保证发送数据的完整性
答:在做水下无人机的蓝牙发送指令给摄像头时,存在一个问题就是发送的指令过长,大约200字节左右,但是海思提供的摄像头蓝牙内部能接收的缓冲区长度只有16~18字节左右的长度,所以做 了一个分包发送的操作,保证了数据的完整性。_limitLength表示自定义每次发包限制的长度大小。系统提供了函数可根据写入类型CBCharacteristicWriteWithResponse、CBCharacteristicWriteWithoutResponse获取最大的写入长度:- (NSUInteger)maximumWriteValueLengthForType:(CBCharacteristicWriteType)type
if (data.length <= _limitLength) {
_results = completion;
[_connectedPerpheral writeValue:data forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
_writeCount ++;
//根据接收模块的处理能力做相应延时,因为蓝牙设备处理指令需要时间,所以我这边给了400~500毫秒
usleep(400 * 1000);
} else {
// 分段发送
NSInteger index = 0;
for (index = 0; index < data.length - _limitLength; index += _limitLength) {
NSData *subData = [data subdataWithRange:NSMakeRange(index, _limitLength)];
[_connectedPerpheral writeValue:subData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
_writeCount++;
usleep(400 * 1000);
}
_results = completion;
NSData *leftData = [data subdataWithRange:NSMakeRange(index, data.length - index)];
if (leftData) {
[_connectedPerpheral writeValue:leftData forCharacteristic:dict[@"character"] type:[dict[@"type"] integerValue]];
_writeCount++;
usleep(400 * 1000);
}
}
问题10:如何实现重连机制
答: 文中提供的重连机制是,自动重连函数被调用之后,会设置一个全局标识为_isAutoConnect=YES,然后判断手机设备的蓝牙是否开启,若开启,则重连扫描外设设备,当扫到上一次连接的蓝牙设备后就会调用连接的代理函数,并停止扫描。
原理:如果手动杀掉APP,那么再次打开APP的时候APP是不会自动连接设备的,但是由于系统蓝牙此时还是与手表连接中的,所以需要重新扫描设备(因为在扫描的代理函数中添加了自动连接的逻辑),经过测试,当扫描到上次连接上的蓝牙外设后就会停止。
方式一:直接扫描重连
// 公共的 自动重连函数
- (void)fs_autoConnectPreviousPeripheral:(FSConnectPeripheralCompletion)completion {
_connectCompletion = completion;
_isAutoConnect = YES;
if (_centralManager.state == CBManagerStatePoweredOn) {
[_centralManager scanForPeripheralsWithServices:nil options:nil];
}
}
在函数- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI中实现。
// 自动连接上一次连接的外设
if (_isAutoConnect) {
NSString *uuid = [self fs_previousConnectionPeripheralUUID];
if ([peripheral.identifier.UUIDString isEqualToString:uuid]) {
peripheral.delegate = self;
[_centralManager connectPeripheral:peripheral options:nil];
}
}
方式二:通过系统提供的函数retrieveConnectedPeripheralsWithServices
NSArray *temp = [_centralManager retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:ServiceUUID]]];
if(temp.count>0) {
CBPeripheral *per = temp[0];
per.delegate = self;
[_centralManager connectPeripheral:peripheral options:nil];
}
方式三:通过系统提供的函数retrievePeripheralsWithIdentifiers
NSArray<CBPeripheral *> *knownPeripherals = [_centralManager retrievePeripheralsWithIdentifiers:@[peripheral.identifier]];
if (knownPeripherals.count == 0) {
return;
}
self.peripheral = knownPeripherals[0];
self.peripheral.delegate = self;
[_centralManager connectPeripheral:self.peripheral
options:@{CBConnectPeripheralOptionNotifyOnDisconnectionKey: @YES}];
问题11:如何获取已经配对过的蓝牙外设
答: 系统一共提供了两个函数来获取已经配对过的蓝牙外设,NSArray *[_centralManager retrieveConnectedPeripheralsWithServices:<#(nonnull NSArray<CBUUID *> *)#>];( CBUUID指的是ServiceUUID)、[_centralManager retrievePeripheralsWithIdentifiers:<#(nonnull NSArray<NSUUID *> *)#>];参数是个已连接的ServiceUUID或Identifiers的数组,是个必填项,若传@[]空数组,则返回值是nil。
问题12:开发蓝牙 APP,有什么工具可以协助蓝牙测试
答: 首先测试蓝牙必须时真机,其次安装了蓝牙调试助手或LightBlue等第三方App来调试蓝牙的开发
IMG_9657.PNG
问题13:App作为中心设备端,连接到蓝牙设备之后,如何获取外设设备的Mac地址。
答:iOS端是无法直接获取设备的Mac地址,但是可以间接获取,但都需要和硬件工程师进行沟通。
1,将蓝牙外设广播里,提供Mac地址,这样中心设备端在扫描阶段,可以直接读取广播里的值,从而获取到外设设备的Mac地址。
2,可以在外设设备的某个服务的特征中,提供Mac地址,但是前提是要确定是读取哪个特征,UUID是多少。
问题14:为什么两个 iPhone 手机的都打开蓝牙之后,却相互搜不到彼此手机上的同个蓝牙Demo。
答:在蓝牙通信中,分为中心端和设备端。而通常手机蓝牙Demo都处在中心端状态,也就是只能接收广播,而自己没有向周围发送广播。所以两台手机之间一般是无法发现对方的(因为大家都是中心端)。
十二、阶段性总结
上述代码基本完成了App扫描外设设备、连接外设设备到发送数据的基本流程,需要深化的点在用户体验相关,比如:连接超时后的处理等。后续分享会加入发送数据后的打印操作,待续。