• iOS学习笔记30-系统服务(三)蓝牙


    一、蓝牙

    随着蓝牙低功耗技术BLE(Bluetooth Low Energy)的发展,蓝牙技术正在一步步成熟,现在的大部分移动设备都配备有蓝牙4.0,相比之前的蓝牙技术耗电量大大减少。

    在iOS中进行蓝牙传输经常使用的框架有例如以下几种:
    1. GameKit.framework
      iOS7之前的蓝牙框架,仅仅可用于同应用之间的蓝牙传输。

    2. MultipeerConnectivity.framework
      iOS7開始引入的蓝牙框架。用于代替GameKit。也有缺陷。仅支持iOS设备之间蓝牙传输。

    3. CoreBluetooth.framework
      功能强大的蓝牙框架。但要求设备必须支持蓝牙4.0。能够支持全部设备蓝牙传输。仅仅要该设备支持蓝牙4.0。

    应用的比較多的是CoreBluetooth框架,这里就选择CoreBluetooth框架来讲。

    二、CoreBluetooth

    当前BLE应用相当广泛。不再仅仅是两个设备之间的传输数据,它还有非常多其它应用市场,比如室内定位、无线支付、智能家居等等,这也使得CoreBluetooth成为当前最热门的蓝牙技术。


    我的理解中。CoreBluetooth蓝牙通信过程和网络通信过程比較相似。

    CoreBluetooth中。蓝牙传输都分为两个部分:
    1. 外围设备CBPeripheral
      负责公布并广播服务,告诉周围的中央设备它的可用服务和特征。相似于网络通信中的服务端。
    2. 中央设备CBCentral
      负责和外围设备建立连接,一旦连接成功就能够使用外围设备的服务和特征,相似于网络通信中的client

    外围设备和中央设备交互的桥梁是服务特征,两个都有一个唯一标识CBUUID来确定一个服务或者特征:
    * 服务CBService
    中央设备仅仅有通过服务才干与外围设备进行传输数据,相似于client通过网址URL才干与server连接一样
    * 特征CBCharacteristic
    每一个服务能够拥有多个特征。中央设备必须订阅外围设备服务的特征值。才干获取外围设备的数据,相似于GET请求能够请求获取server数据,POST请求能够向server传输数据一样。

    三、设备作为外围设备

    设备作为外围设备的创建步骤:
    1. 创建外围设备管理器CBPeripheralManager,并设置代理
    2. 创建一个特征CBCharacteristic。绑定一个CBUUID,设置特征属性
    3. 创建一个服务CBService,绑定一个CBUUID,设置服务的特征
    4. 调用外围设备管理器的对象方法。加入服务到外围设备上
    -(void)addService:(CBService *)service;
    1. 外围设备管理器開始广播服务
    -(void)startAdvertising:(NSDictionary *)dict;
    1. 在外围设备管理器的代理方法。处理与中央设备的交互

    外围设备管理器启动服务的相关代理方法:
    /* 外围设备管理器状态发生改变后调用,比方外围设备打开蓝牙的时候 */
    - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral;
    /* 外围设备管理器加入了服务后调用,一般在这里进行广播服务 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
                didAddService:(CBService *)service /* 加入的服务 */
                        error:(NSError *)error;/* 加入服务错误信息 */
    /* 启动广播服务后调用 */
    - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral 
                                           error:(NSError *)error;/* 启动服务错误信息 */
    /* 外围设备恢复状态时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
             willRestoreState:(NSDictionary *)dict;
    外围设备管理器和中央设备进行交互的代理方法:
    /* 中央设备订阅外围设备的特征时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral /* 外围设备管理器 */
                      central:(CBCentral *)central /* 中央设备 */
            didSubscribeToCharacteristic:(CBCharacteristic *)characteristic;/* 订阅的特征 */
    /* 中央设备取消订阅外围设备的特征时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral /* 外围设备管理器 */
                      central:(CBCentral *)central /* 中央设备 */
            didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic;/* 特征 */
    /* 外围设备收到中央设备的写请求时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
            didReceiveWriteRequests:(CBATTRequest *)request;/* 写请求 */
    外围设备管理器CBPeripheralManager的经常使用对象方法:
    /* 加入服务 */
    - (void)addService:(CBService *)service;
    /* 开启广播服务,dict设置设备信息 */
    - (void)startAdvertising:(NSDictionary *)dict;
    /* 更新特征值,centrals为空,表示对全部连接的中央设备通知 */
    - (void)updateValue:(NSData *)value /* 特征的特征值 */
            forCharacteristic:(CBCharacteristic *)characteristic /* 特征 */
            onSubscribedCentrals:(NSArray *)centrals;/* 须要通知更新特征值的中央设备 */
    以下是设备作为外围设备的实例:
    #import "PeripheralViewController.h"
    #import <CoreBluetooth/CoreBluetooth.h>
    
    #define kPeripheralName         @"Liuting's Device" //外围设备名称。自己定义
    #define kServiceUUID           @"FFA0-FFB0" //服务的UUID,自己定义
    #define kCharacteristicUUID     @"FFCC-FFDD" //特征的UUID,自己定义
    
    @interface PeripheralViewController ()<CBPeripheralManagerDelegate>
    @property (strong, nonatomic) CBPeripheralManager *peripheralManager;/* 外围设备管理器 */
    @property (strong, nonatomic) NSMutableArray *centralM;/* 订阅的中央设备 */
    @property (strong, nonatomic) CBMutableCharacteristic *characteristicM;/* 特征 */
    @end
    @implementation PeripheralViewController
    - (void)viewDidLoad{
        [super viewDidLoad];
        self.centralM = [NSMutableArray array];
        //创建外围设备管理器
        self.peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self
                                                                         queue:nil];
    }
    #pragma mark - UI事件
    /* 点击更新特征值 */
    - (IBAction)changeCharacteristicValue:(id)sender {
        //特征值,这里是更新特征值为当前时间
        NSString *valueStr = [NSString stringWithFormat:@"%@",[NSDate date]];
        NSData *value = [valueStr dataUsingEncoding:NSUTF8StringEncoding];
        //更新特征值
        [self.peripheralManager updateValue:value 
                          forCharacteristic:self.characteristicM 
                       onSubscribedCentrals:nil];
    }
    #pragma mark - 私有方法
    /* 创建特征、服务并加入服务到外围设备 */
    - (void)addMyService{
        /*1.创建特征*/
        //创建特征的UUID对象
        CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID];
        /* 创建特征
         * 參数
         * uuid:特征标识
         * properties:特征的属性,比如:可通知、可写、可读等
         * value:特征值
         * permissions:特征的权限
         */
        CBMutableCharacteristic *characteristicM = 
            [[CBMutableCharacteristic alloc] initWithType:characteristicUUID 
                                               properties:CBCharacteristicPropertyNotify
                                                    value:nil 
                                              permissions:CBAttributePermissionsReadable];
        self.characteristicM = characteristicM;
        //创建服务UUID对象
        CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
        //创建服务
        CBMutableService *serviceM = [[CBMutableService alloc] initWithType:serviceUUID 
                                                                    primary:YES];    
        //设置服务的特征
        [serviceM setCharacteristics:@[characteristicM]];
        //将服务加入到外围设备
        [self.peripheralManager addService:serviceM];
    }
    
    #pragma mark - CBPeripheralManager代理方法
    /* 外围设备状态发生变化后调用 */
    - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral{
        //推断外围设备管理器状态
        switch (peripheral.state) {
            case CBPeripheralManagerStatePoweredOn:
            {
                NSLog(@"BLE已打开.");
                //加入服务
                [self addMyService];
                break;
            }
            default:
            {   
                NSLog(@"此设备不支持BLE或未打开蓝牙功能,无法作为外围设备.");
                break;
            }
        }
    }
    /* 外围设备恢复状态时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
             willRestoreState:(NSDictionary *)dict
    {
        NSLog(@"状态恢复");
    }
    /* 外围设备管理器加入服务后调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
                didAddService:(CBService *)service 
                       error:(NSError *)error
    {
        //设置设备信息dict。CBAdvertisementDataLocalNameKey是设置设备名
        NSDictionary *dict = @{CBAdvertisementDataLocalNameKey:kPeripheralName};
        //開始广播
        [self.peripheralManager startAdvertising:dict];
        NSLog(@"向外围设备加入了服务并開始广播...");
    }
    /* 外围设备管理器启动广播后调用 */
    - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral 
                                           error:(NSError *)error
    {
        if (error) {
            NSLog(@"启动广播过程中错误发生。错误信息:%@",error.localizedDescription);
            return;
        }
        NSLog(@"启动广播...");
    }
    /* 中央设备订阅特征时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
                      central:(CBCentral *)central 
            didSubscribeToCharacteristic:(CBCharacteristic *)characteristic
    {    
        NSLog(@"中心设备:%@ 已订阅特征:%@.",central,characteristic);
        //把订阅的中央设备存储下来
        if (![self.centralM containsObject:central]) {
            [self.centralM addObject:central];
        }
    }
    /* 中央设备取消订阅特征时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
                     central:(CBCentral *)central 
            didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic
    {
        NSLog(@"中心设备:%@ 取消订阅特征:%@",central,characteristic);
    }
    
    /* 外围设备管理器收到中央设备写请求时调用 */
    - (void)peripheralManager:(CBPeripheralManager *)peripheral 
            didReceiveWriteRequests:(CBATTRequest *)request
    {    
        NSLog(@"收到写请求");
    }
    @end

    四、设备作为中央设备

    很多其它的时候,我们须要的是一个中央设备,外围设备不一定是iOS设备,所以上面外围设备的创建不一定会用到。比方外围设备是GPS导航仪、心率仪等。这些仅仅要遵循BLE4.0的规范,中央设备就能够与之连接并寻找服务和订阅特征。

    设备作为中央设备的创建步骤:
    1. 创建中央设备管理器对象CBCentralManager,设置代理
    2. 扫描外围设备,发现外围设备CBPeripheral进行连接,保持连接的外围设备
    3. 在连接外围设备成功的代理方法中,设置外围设备的代理。调用外围设备的对象方法寻找服务
    4. 查找外围设备的服务和特征。查找到可用特征,则读取特征数据。

      • **记住这里是外围设备对象CBPeripheral
        不是上面的外围设备管理器对象CBPeripheralManager**
    中央设备管理器的代理方法:
    /* 中央设备管理器状态改变后调用,比方蓝牙的打开与关闭 */
    - (void)centralManagerDidUpdateState:(CBCentralManager *)central;
    /* 开启扫描后。中央设备管理器发现外围设备后调用 */
    - (void)centralManager:(CBCentralManager *)central 
            didDiscoverPeripheral:(CBPeripheral *)peripheral /* 外围设备 */
                advertisementData:(NSDictionary *)advertisementData /* 设备信息 */
                             RSSI:(NSNumber *)RSSI; /* 信号强度 */
    /* 中央设备管理器成功连接到外围设备后调用 */
    - (void)centralManager:(CBCentralManager *)central 
      didConnectPeripheral:(CBPeripheral *)peripheral;/* 外围设备 */
    /* 中央设备管理器连接外围设备失败后调用 */
    - (void)centralManager:(CBCentralManager *)central 
            didFailToConnectPeripheral:(CBPeripheral *)peripheral /* 外围设备 */
                                 error:(NSError *)error;/* 连接失败的错误信息 */
    中央设备管理器的对象方法:
    /* 扫描外围设备,能够指定含有指定服务的外围设备 */
    - (void)scanForPeripheralsWithServices:(NSArray<CBUUID *> *)services 
                                   options:(NSDictionary *)options;
    /* 停止扫描 */
    - (void)stopScan;
    /* 连接外围设备 */
    - (void)connectPeripheral:(CBPeripheral *)peripheral 
                      options:(NSDictionary *)options;
    /* 断开外围设备 */
    - (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
    外围设备的代理方法【和上面的外围设备管理器代理不一样】
    /**
     *  1.成功订阅外围设备的服务后调用,在该代理方法中寻找服务的特征
     *  @param peripheral 连接到的设备
     *  @param error       错误信息
     */
    - (void)peripheral:(CBPeripheral *)peripheral 
            didDiscoverServices:(NSError *)error;
    /**
     *  2.成功找到外围设备服务的特征后调用,在该代理方法中设置订阅方式
     *  @param peripheral 连接的设备
     *  @param service     拥有的服务
     *  @param error       错误信息
     */
    - (void)peripheral:(CBPeripheral *)peripheral 
            didDiscoverCharacteristicsForService:(CBService *)service 
                 error:(NSError *)error;
    /**
     *  3.外围设备读取到特征值就会调用
     *  @param peripheral    连接的设备
     *  @param characteristic 改变的特征
     *  @param error          错误信息
     */
    - (void)peripheral:(CBPeripheral *)peripheral 
            didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic 
                 error:(NSError *)error;
    /**
     *  4.向外围设备的特征对象写操作完毕后调用
     *  特别:当写操作为CBCharacteristicWriteWithoutResponse时不会调用
     *  @param peripheral    连接的设备
     *  @param characteristic 要写入的特征
     *  @param error          错误信息
     */
    - (void)peripheral:(CBPeripheral *)peripheral 
            didWriteValueForCharacteristic:(CBCharacteristic *)characteristic 
                 error:(NSError *)error;
    外围设备CBPeripheral的经常使用对象方法:
    /* 寻找服务,传入的是服务的唯一标识CBUUID */
    - (void)discoverServices:(NSArray<CBUUID *> *)services;
    /* 寻找指定服务下的特征。特征数组也是传入特征的唯一标识CBUUID */
    - (void)discoverCharacteristics:(NSArray<CBUUID *> *)characteristics 
                         forService:(CBService *)service;/* 服务 */
    /* 设置是否向特征订阅数据实时通知,YES表示会实时多次会调用代理方法读取数据 */
    - (void)setNotifyValue:(BOOL)value 
         forCharacteristic:(CBCharacteristic *)characteristic;
    /* 读取特征的数据,调用此方法后会调用一次代理方法读取数据 */
    - (void)readValueForCharacteristic:(CBCharacteristic *)characteristic;
    /* 向特征写入数据,看type类型。决定调不调用写入数据后回调的代理方法 */
    - (void)writeValue:(NSData *)value /* 写入数据 */
     forCharacteristic:(CBCharacteristic *)characteristic /* 特征 */
                  type:(CBCharacteristicWriteType)type;/* 写入类型 */
    写入类型眼下仅仅有2种方式:
    /* 写入类型,决定要不要调用代理方法 */
    typedef NS_ENUM(NSInteger, CBCharacteristicWriteType) { 
        CBCharacteristicWriteWithResponse = 0, //有回调的写入
        CBCharacteristicWriteWithoutResponse //没回调的写入
    };
    以下是设备作为中央设备的实例:
    #import "CentralViewController.h"
    #import <CoreBluetooth/CoreBluetooth.h>
    
    #define kPeripheralName         @"Liuting's Device" //外围设备名称
    #define kServiceUUID           @"FFA0-FFB0" //服务的UUID
    #define kCharacteristicUUID     @"FFCC-FFDD" //特征的UUID
    
    @interface CentralViewController ()<CBCentralManagerDelegate,CBPeripheralDelegate>
    @property (strong, nonatomic) CBCentralManager *centralManager;/* 中央设备管理器 */
    @property (strong, nonatomic) NSMutableArray *peripherals;/* 连接的外围设备 */
    @end
    @implementation CentralViewController
    #pragma mark - UI事件
    - (void)viewDidLoad{
        [super viewDidLoad];
        self.peripherals = [NSMutableArray array];
        //创建中心设备管理器并设置当前控制器视图为代理
        self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    }
    #pragma mark - CBCentralManager代理方法
    /* 中央设备管理器状态更新后调用 */
    - (void)centralManagerDidUpdateState:(CBCentralManager *)central{
        switch (central.state) {
        case CBPeripheralManagerStatePoweredOn:
            NSLog(@"BLE已打开.");
            //扫描外围设备 
            [central scanForPeripheralsWithServices:nil options:nil];
            break;
        default:
            NSLog(@"此设备不支持BLE或未打开蓝牙功能,无法作为中央设备.");
            break;
        }
    }
    /*
     *  发现外围设备调用
     *  @param central           中央设备管理器
     *  @param peripheral        外围设备
     *  @param advertisementData 设备信息
     *  @param RSSI              信号质量(信号强度)
     */
    - (void)centralManager:(CBCentralManager *)central 
            didDiscoverPeripheral:(CBPeripheral *)peripheral 
                advertisementData:(NSDictionary *)advertisementData 
                             RSSI:(NSNumber *)RSSI
    {    
        NSLog(@"发现外围设备...");
        //连接指定的外围设备。匹配设备名
        if ([peripheral.name isEqualToString:kPeripheralName]) {
            //加入保存外围设备,由于在此方法调用完外围设备对象就会被销毁
            if(![self.peripherals containsObject:peripheral]){
                [self.peripherals addObject:peripheral];
            }        
            NSLog(@"開始连接外围设备...");
            [self.centralManager connectPeripheral:peripheral options:nil];
        }
    }
    /* 中央设备管理器成功连接到外围设备后调用 */
    - (void)centralManager:(CBCentralManager *)central 
            didConnectPeripheral:(CBPeripheral *)peripheral
            {
        NSLog(@"连接外围设备成功!");
    
        //停止扫描
        [self.centralManager stopScan];
        //设置外围设备的代理为当前视图控制器
        peripheral.delegate = self;
        //外围设备開始寻找服务
        [peripheral discoverServices:@[[CBUUID UUIDWithString:kServiceUUID]]];
    }
    /* 中央设备管理器连接外围设备失败后调用 */
    - (void)centralManager:(CBCentralManager *)central 
            didFailToConnectPeripheral:(CBPeripheral *)peripheral
                                 error:(NSError *)error
     {
         NSLog(@"连接外围设备失败!");
     }
     #pragma mark - CBPeripheral 代理方法
    /* 外围设备寻找到服务后调用 */
    - (void)peripheral:(CBPeripheral *)peripheral 
            didDiscoverServices:(NSError *)error
    {    
        NSLog(@"已发现可用服务...");
        //遍历查找到的服务
        CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
        CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID];
        for (CBService *service in peripheral.services) {
            if([service.UUID isEqual:serviceUUID]){
                //外围设备查找指定服务中的特征,characteristics为nil。表示寻找全部特征
                [peripheral discoverCharacteristics:nil forService:service];
            }
        }
    }
    /* 外围设备寻找到特征后调用 */
    - (void)peripheral:(CBPeripheral *)peripheral 
            didDiscoverCharacteristicsForService:(CBService *)service 
                 error:(NSError *)error
    {
        NSLog(@"已发现可用特征...");
        //遍历服务中的特征
        CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID];
        CBUUID *characteristicUUID = [CBUUID UUIDWithString:kCharacteristicUUID];
        if ([service.UUID isEqual:serviceUUID]) {
            for (CBCharacteristic *characteristic in service.characteristics) {
                if ([characteristic.UUID isEqual:characteristicUUID]) {
                    //情景一:通知
                    /* 找到特征后设置外围设备为已通知状态(订阅特征):
                    * 调用此方法会触发代理方法peripheral:didUpdateValueForCharacteristic:error: 
                    * 调用此方法会触发外围设备管理器的订阅代理方法
                    */
                    [peripheral setNotifyValue:YES forCharacteristic:characteristic];
    
                    //情景二:读取
                    //调用此方法会触发代理方法peripheral:didUpdateValueForCharacteristic:error:
                    //[peripheral readValueForCharacteristic:characteristic]; 
                }
            } 
        }
    }
    /* 外围设备读取到特征值后调用 */
    - (void)peripheral:(CBPeripheral *)peripheral 
            didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
                 error:(NSError *)error
    {
        if (characteristic.value) { 
            NSString *value = [[NSString alloc] initWithData:characteristic.value 
                                                    encoding:NSUTF8StringEncoding];
            NSLog(@"读取到特征值:%@",value); 
        }else{
            NSLog(@"未发现特征值."); 
        }
    }
    @end

    五、蓝牙后台执行

    除非去申请后台权限,否则 app 都是仅仅在前台执行的,程序在进入后台不久便会切换到挂起状态。挂起后,程序将无法再接收不论什么蓝牙事件。

    中央设备管理器连接外围设备的方法中的options属性,能够设置例如以下字典值:
    • CBConnectPeripheralOptionNotifyOnConnectionKey
      在连接成功后。程序被挂起。给出系统提示。
    • CBConnectPeripheralOptionNotifyOnDisconnectionKey
      在程序挂起,蓝牙连接断开时,给出系统提示。
    • CBConnectPeripheralOptionNotifyOnNotificationKey
      在程序挂起后,收到 peripheral 数据时。给出系统提示。

    设置蓝牙后台模式:
    • 加入info.plist字段Required background nodes
    • 在该字段下加入字符串值:
      • App communicates using CoreBluetooth:表示支持设备作为中央设备后台执行
      • App shares data using CoreBluetooth:表示支持设备作为外围设备后台执行

    后台执行设置info.plist

    设备作为中央设备的后台执行和前台执行差别:
    • 会将发现的多个外围设备的广播数据包合并为一个事件,然后每找到一个外围设备都会调用发现外围设备的代理方法
    • 假设全部的应用都在后台搜索外围设备。那么每次搜索的时间间隔会更大。这会导致搜索到外围设备的时间变长
    设备作为外围设备的后台执行和前台执行差别:
    • 在广播时,广播数据将不再包括外围设备的名字
    • 外围设备仅仅能被明白标识了搜索服务UUID的iOS设备找到
    • 假设全部应用都在后台发起广播,那么发起频率会减少

    关于后台执行的细节。能够參考:CoreBluetooth8 后台执行蓝牙服务
    上面的代码Demo点这里:LearnDemo里面的BluetoothDemo

    有什么问题在下方评论区中提出!O(∩_∩)O哈。
  • 相关阅读:
    20220501 08:00:01
    20220508 08:00:01
    20220505 08:00:01
    20220430 08:00:01
    20220509 08:00:01
    20220507 08:00:01
    20220503 08:00:01
    20220506 08:00:01
    substrate学习编写简单pallet的问题记录
    Rust内存Layout
  • 原文地址:https://www.cnblogs.com/yutingliuyl/p/7306172.html
Copyright © 2020-2023  润新知