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)外设设备(可根据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扫描外设设备、连接外设设备到发送数据的基本流程,需要深化的点在用户体验相关,比如:连接超时后的处理等。后续分享会加入发送数据后的打印操作,待续。

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

推荐阅读更多精彩内容