iOS 蓝牙开发技术分享

一、技术背景

本文主要是从蓝牙的扫描、连接、收发数据、打印等方向快速熟悉蓝牙开发,记录了在开发过程中遇到的的问题及解决方法。在分享之前,我们需要清楚几个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)外设设备(可根据serviceUUID来辨别是否是我们连接的设备)
  • 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<CBPeripheral *> *)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> *)identifiers NS_AVAILABLE(10_9, 7_0);

- (NSArray<CBPeripheral *> *)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> *)serviceUUIDs NS_AVAILABLE(10_9, 7_0);
  • 4、扫描外设(如果参数传nil,表示扫描所有外设)和停止扫描
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;

- (void)stopScan;
  • 5、连接指定外设
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)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<NSString *, id> *)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<CBService *> *services;
  • 5、发现服务
- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;
  • 6、发现子服务
- (void)discoverIncludedServices:(nullable NSArray<CBUUID *> *)includedServiceUUIDs forService:(CBService *)service;
  • 7、发现特征
- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)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<CBService *> *)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、筛选出peripheralnil的外设信息
    • 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。
    :当首次调用函数搜索设备外设时,无法获取外设设备信息的原因是centralstateCBCentralManagerStateUnknown,这个状态表示手机设备的蓝牙状态为未开启。解决方法:需要在此委托方法中监听蓝牙状态的状态改变为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扫描外设设备、连接外设设备到发送数据的基本流程,需要深化的点在用户体验相关,比如:连接超时后的处理等。后续分享会加入发送数据后的打印操作,待续。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容