zoukankan      html  css  js  c++  java
  • 【转】Core Bluetooth框架之一:Central与Peripheral

    原文网址:http://southpeak.github.io/blog/2014/07/29/core-bluetoothkuang-jia-zhi-%5B%3F%5D-:centralyu-peripheral/

    iOS和Mac应用使用Core Bluetooth framework来与BLE(低功耗蓝牙)设备通信。我们的程序可以发现、搜索并与低功耗外围(Peripheral)蓝牙设备通信,如心跳监听器、数字温控器、甚至是其它iOS设备。这个框架抽象了支持蓝牙4.0标准低功耗设备的基本操作,隐藏了4.0标准的底层实现细节,让我们可以方便的使用BLE设备。

    蓝牙通信中的角色

    在BLE通信中,主要有两个角色:Central和Peripheral。类似于传统的客户端-服务端架构,一个Peripheral端是提供数据的一方(相当于服务端);而Central是使用Peripheral端提供的数据完成特定任务的一方(相当于客户端)。下图以心跳监听器为例展示了这样一个架构:

    image

    Peripheral端以广告包的形式来广播一些数据。一个广告包(advertising packet)是一小束相关数据,可能包含Peripheral提供的有用的信息,如Peripheral名或主要功能。在BLE下,广告是Peripheral设备表现的主要形式。

    Central端可以扫描并监听其感兴趣的任何广播信息的Peripheral设备。

    数据的广播及接收需要以一定的数据结构来表示。而服务就是这样一种数据结构。Peripheral端可能包含一个或多个服务或提供关于连接信号强度的有用信息。一个服务是一个设备的数据的集合及数据相关的操作。

    而服务本身又是由特性或所包含的服务组成的。一个特性提供了关于服务的更详细的信息。下图展示了心率监听器中的各种数据结构

    image

    在一个Central端与Peripheral端成功建立连接后,Central可以发现Peripheral端提供的完整的服务及特性的集合。一个Central也可以读写Peripheral端的服务特性的值。我们将会在下面详细介绍。

    Central、Peripherals及Peripheral数据的表示

    当我们使用本地Central与Peripheral端交互时,我们会在BLE通信的Central端执行操作。除非我们设置了一个本地Peripheral设备,否则大部分蓝牙交互都是在Central端进行的。(下文也会讲Peripheral端的基本操作)

    在Central端,本地Central设备由CBCentralManager对象表示。这个对象用于管理发现与连接Peripheral设备(CBPeripheral对象)的操作,包括扫描、查找和连接。下图本地Central端与peripheral对象

    image

    当与peripheral设备交互时,我们主要是在处理它的服务及特性。在Core Bluetooth框架中,服务是一个CBService对象,特性是一个CBCharacteristic对象,下图演示了Central端的服务与特性的基本结构:

    image

    苹果在OS X 10.9和iOS 6版本后,提供了BLE外设(Peripheral)功能,可以将设备作为Peripheral来处理。在Peripheral端,本地Peripheral设备表示为一个CBPeripheralManager对象。这些对象用于管理将服务及特性发布到本地Peripheral设备数据库,并广告这些服务给Central设备。Peripheral管理器也用于响应来自Central端的读写请求。如下图展示了一个Peripheral端角色:

    image

    当在本地Peripheral设备上设置数据时,我们实际上处理的是服务与特性的可变版本。在Core Bluetooth框架中,本地Peripheral服务由CBMutableService对象表示,而特性由CBMutableCharacteristic对象表示,下图展示了本地Peripheral端服务与特性的基本结构:

    image

    Peripheral(Server)端操作

    一个Peripheral端操作主要有以下步骤:

    1. 启动一个Peripheral管理对象
    2. 在本地Peripheral中设置服务及特性
    3. 将服务及特性发布给设备的本地数据库
    4. 广告我们的服务
    5. 针对连接的Central端的读写请求作出响应
    6. 发送更新的特性值到订阅Central端

    我们将在下面结合代码对每一步分别进行讲解

    启动一个Peripheral管理器

    要在本地设备上实现一个Peripheral端,我们需要分配并初始化一个Peripheral管理器实例,如下代码所示

    // 创建一个Peripheral管理器
    // 我们将当前类作为peripheralManager,因此必须实现CBPeripheralManagerDelegate
    // 第二个参数如果指定为nil,则默认使用主队列
    peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
    

    创建Peripheral管理器后,Peripheral管理器会调用代理对象的peripheralManagerDidUpdateState:方法。我们需要实现这个方法来确保本地设备支持BLE。

    - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
    {
        NSLog(@"Peripheral Manager Did Update State");
        switch (peripheral.state) {
            case CBPeripheralManagerStatePoweredOn:
                NSLog(@"CBPeripheralManagerStatePoweredOn");
                break;
    
            case CBPeripheralManagerStatePoweredOff:
                NSLog(@"CBPeripheralManagerStatePoweredOff");
                break;
    
            case CBPeripheralManagerStateUnsupported:
                NSLog(@"CBPeripheralManagerStateUnsupported");
                break;
    
            default:
                break;
        }
    }
    

    设置服务及特性

    一个本地Peripheral数据库以类似树的结构来组织服务及特性。所以,在设置服务及特性时,我们将其组织成树结构。

    一个Peripheral的服务和特性通过128位的蓝牙指定的UUID来标识,该标识是一个CBUUID对象。虽然SIG组织没的预先定义所有的服务与特性的UUID,但是SIG已经定义并发布了一些通过的UUID,这些UUID被简化成16位以方便使用。例如,SIG定义了一个16位的UUID作为心跳服务的标识(180D)。

    CBUUID类提供了方法,以从字符串中生成一个CBUUID对象。当字条串使用的是预定义的16位UUID时,Core Bluetooth使用它时会预先自动补全成128位的标识。

    CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString:@"180D"];
    

    当然我们也可以自己生成一个128位的UUID来标识我们的服务与特性。在命令行中使用uuidgen命令会生成一个128位的UUID字符串,然后我们可以使用它来生成一个CBUUID对象。

    生成UUID对象后,我们就可以用这个对象来创建我们的服务及特性,然后再将它们组织成树状结构。

    创建特性的代码如下所示

    CBUUID *characteristicUUID1 = [CBUUID UUIDWithString:@"C22D1ECA-0F78-463B-8C21-688A517D7D2B"];
    CBUUID *characteristicUUID2 = [CBUUID UUIDWithString:@"632FB3C9-2078-419B-83AA-DBC64B5B685A"];
    
    CBMutableCharacteristic *character1 = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID1 properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable];
    
    CBMutableCharacteristic *character2 = [[CBMutableCharacteristic alloc] initWithType:characteristicUUID2 properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsWriteable];
    

    我们需要设置特性的属性、值及权限。属性及权限值确定了属性值是可读的还是可写的,及连接的Central端是否可以订阅特性的值。另外,如果我们指定了特性的值,则这个值会被缓存且其属性及权限被设置成可读的。如果我们要让特性的值是可写的,或者期望属性所属的服务的生命周期里这个值可以被修改,则必须指定值为nil。

    创建的特性之后,我们便可以创建一个与特性相关的服务,然后将特性关联到服务上,如下代码所示:

    CBUUID *serviceUUID = [CBUUID UUIDWithString:@"3655296F-96CE-44D4-912D-CD83F06E7E7E"];
    CBMutableService *service = [[CBMutableService alloc] initWithType:serviceUUID primary:YES];
    service.characteristics = @[character1, character2];    // 组织成树状结构
    

    上例中primary参数传递的是YES,表示这是一个主服务,即描述了一个设备的主要功能且能被其它服务引用。与之相对的是次要服务(secondary service),其只在引用它的另一个服务的上下文中描述一个服务。

    发布服务及特性

    创建服务及特性后交将其组织成树状结构后,我们需要将这些服务发布到设备的本地数据库上。我们可以使用CBPeripheralManager的addService:方法来完成此工作。如下代码所示:

    [peripheralManager addService:service];
    

    在调用些方法发布服务时,CBPeripheralManager对象会调用它的代理的peripheralManager:didAddService:error:方法。如果发布过程中出现错误导致无法以布,则可以实现该代理方法来处理错误,如下代码所示:

    - (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error
    {
        NSLog(@"Add Service");
    
        if (error)
        {
            NSLog(@"Error publishing service: %@", [error localizedDescription]);
        }
    }
    

    在将服务与特性发布到设备数据库后,服务将会被缓存,且我们不能再修改这个服务。

    广告服务

    处理完以上步骤,我们便可以将这些服务广告给对服务感兴趣的Central端。我们可以通过调用CBPeripheralManager实例的startAdvertising:方法来完成这一操作,如下代码所示:

    [peripheralManager startAdvertising:@{CBAdvertisementDataServiceUUIDsKey: @[service.UUID]}];
    

    startAdvertising:的参数是一个字典,Peripheral管理器支持且仅支持两个key值:CBAdvertisementDataLocalNameKey与CBAdvertisementDataServiceUUIDsKey。这两个值描述了数据的详情。key值所对应的value期望是一个表示多个服务的数组。

    当广告服务时,CBPeripheralManager对象会调用代码对象的peripheralManagerDidStartAdvertising:error:方法,我们可以在此做相应的处理,如下代码所示:

    - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error
    {
        NSLog(@"Start Advertising");
    
        if (error)
        {
            NSLog(@"Error advertising: %@", [error localizedDescription]);
        }
    }
    

    广告服务之后,Central端便可以发现设备并初始化一个连接。

    对Central端的读写请求作出响应

    在与Central端进行连接后,可能需要从其接收读写请求,我们需要以适当的方式作出响应。

    当连接的Central端请求读取特性的值时,CBPeripheralManager对象会调用代理对象的peripheralManager:didReceiveReadRequest:方法,代理方法提供一个CBATTRequest对象以表示Central端的请求,我们可以使用它的属性来填充请求。下面代码简单展示了这样一个过程:

    - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request
    {
        // 查看请求的特性是否是指定的特性
        if ([request.characteristic.UUID isEqual:cha1.UUID])
        {
            NSLog(@"Request character 1");
    
            // 确保读请求所请求的偏移量没有超出我们的特性的值的长度范围
            // offset属性指定的请求所要读取值的偏移位置
            if (request.offset > cha1.value.length)
            {
                [peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
                return;
            }
    
            // 如果读取位置未越界,则将特性中的值的指定范围赋给请求的value属性。
            request.value = [cha1.value subdataWithRange:(NSRange){request.offset, cha1.value.length - request.offset}];
    
            // 对请求作出成功响应
            [peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
        }
    }
    

    在每次调用代理对象的peripheralManager:didReceiveReadRequest:时调用respondToRequest:withResult:方法以对请求做出响应。

    处理写请求类似于上述过程,此时会调用代理对象的peripheralManager:didReceiveWriteRequests:方法。不同的是代理方法会给我们一个包含一个或多个CBATTRequest对象的数组,每一个都表示一个写请求。我们可以使用请求对象的value属性来给我们的特性属性赋值,如下代码所示:

    - (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests
    {
        CBATTRequest *request = requests[0];
    
        cha1.value = request.value;
    
        [peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
    }
    

    响应处理与请求类似。

    发送更新的特性值给订阅的Central端

    如果有一个或多个Central端订阅了我们的服务的特性时,当特性发生变化时,我们需要通知这些Central端。为此,代理对象需要实现peripheralManager:central:didSubscribeToCharacteristic:方法。如下所示:

    - (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic
    {
        NSLog(@"Central subscribed to characteristic %@", characteristic);
    
        NSData *updatedData = characteristic.value;
    
        // 获取属性更新的值并调用以下方法将其发送到Central端
        // 最后一个参数指定我们想将修改发送给哪个Central端,如果传nil,则会发送给所有连接的Central
        // 将方法返回一个BOOL值,表示修改是否被成功发送,如果用于传送更新值的队列被填充满,则方法返回NO
        BOOL didSendValue = [peripheralManager updateValue:updatedData forCharacteristic:(CBMutableCharacteristic *)characteristic onSubscribedCentrals:nil];
    
        NSLog(@"Send Success ? %@", (didSendValue ? @"YES" : @"NO"));
    }
    

    在上述代码中,当传输队列有可用的空间时,CBPeripheralManager对象会调用代码对象的peripheralManagerIsReadyToUpdateSubscribers:方法。我们可以在这个方法中调用updateValue:forCharacteristic:onSubscribedCentrals:来重新发送值。

    我们使用通知来将单个数据包发送给订阅的Central。当我们更新订阅的Central时,我们应该通过调用一次updateValue:forCharacteristic:onSubscribedCentrals:方法将整个更新的值放在一个通知中。

    由于特性的值大小不一,所以不是所有值都会被通知传输。如果发生这种情况,需要在Central端调用CBPeripheral实例的readValueForCharacteristic:方法来处理,该方法可以获取整个值。

    Central(Client)端操作

    一个Central端主要包含以下操作:

    1. 启动一个Central端管理器对象
    2. 搜索并连接正在广告的Peripheral设备
    3. 在连接到Peripheral端后查询数据
    4. 发送一个对特性值的读写请求到Peripheral端
    5. 当Peripheral端特性值改变时接收通知

    我们将在下面结合代码对每一步分别进行讲解

    启动一个Central管理器

    CBCentralManager对象在Core Bluetooth中表示一个本地Central设备,我们在执行任何BLE交互时必须分配并初始化一个Central管理器对象。创建代码如下所示:

    // 指定当前类为代理对象,所以其需要实现CBCentralManagerDelegate协议
    // 如果queue为nil,则Central管理器使用主队列来发送事件
    centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
    

    创建Central管理器时,管理器对象会调用代理对象的centralManagerDidUpdateState:方法。我们需要实现这个方法来确保本地设备支持BLE。

    - (void)centralManagerDidUpdateState:(CBCentralManager *)central
    {
        NSLog(@"Central Update State");
    
        switch (central.state) {
            case CBCentralManagerStatePoweredOn:
                NSLog(@"CBCentralManagerStatePoweredOn");
                break;
    
            case CBCentralManagerStatePoweredOff:
                NSLog(@"CBCentralManagerStatePoweredOff");
                break;
    
            case CBCentralManagerStateUnsupported:
                NSLog(@"CBCentralManagerStateUnsupported");
                break;
    
            default:
                break;
        }
    }
    

    发现正在广告的Peripheral设备

    Central端的首要任务是发现正在广告的Peripheral设备,以备后续连接。我们可以调用CBCentralManager实例的scanForPeripheralsWithServices:options:方法来发现正在广告的Peripheral设备。如下代码所示:

    // 查找Peripheral设备
    // 如果第一个参数传递nil,则管理器会返回所有发现的Peripheral设备。
    // 通常我们会指定一个UUID对象的数组,来查找特定的设备
    [centralManager scanForPeripheralsWithServices:nil options:nil];
    

    在调用上述方法后,CBCentralManager对象在每次发现设备时会调用代理对象的centralManager:didDiscoverPeripheral:advertisementData:RSSI:方法。

    - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
    {
        NSLog(@"Discover name : %@", peripheral.name);
    
        // 当我们查找到Peripheral端时,我们可以停止查找其它设备,以节省电量
        [centralManager stopScan];
    
        NSLog(@"Scanning stop");
    }
    

    连接Peripheral设备

    在查找到Peripheral设备后,我们可以调用CBCentralManager实例的connectPeripheral:options:方法来连接Peripheral设备。如下代码所示

    [centralManager connectPeripheral:peripheral options:nil];
    

    如果连接成功,则会调用代码对象的centralManager:didConnectPeripheral:方法,我们可以实现该方法以做相应处理。另外,在开始与Peripheral设备交互之前,我们需要设置peripheral对象的代理,以确保接收到合适的回调。

    - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
    {
        NSLog(@"Peripheral Connected");
    
        peripheral.delegate = self;
    }
    

    查找所连接Peripheral设备的服务

    建立到Peripheral设备的连接后,我们就可以开始查询数据了。首先我们需要查找Peripheral设备中可用的服务。由于Peripheral设备可以广告的数据有限,所以Peripheral设备实际的服务可能比它广告的服务要多。我们可以调用peripheral对象的discoverServices:方法来查找所有的服务。如下代码所示:

    [peripheral discoverServices:nil];
    

    参数传递nil可以查找所有的服务,但一般情况下我们会指定感兴趣的服务。

    当调用上述方法时,peripheral会调用代理对象的peripheral:didDiscoverServices:方法。Core Bluetooth创建一个CBService对象的数组,数组中的元素是peripheral中找到的服务。

    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
    {
        NSLog(@"Discover Service");
    
        for (CBService *service in peripheral.services)
        {
            NSLog(@"Discovered service %@", service);
        }
    }
    

    查找服务中的特性

    假设我们已经找到感兴趣的服务,接下来就是查询服务中的特性了。为了查找服务中的特性,我们只需要调用CBPeripheral类的discoverCharacteristics:forService:方法,如下所示:

    NSLog(@"Discovering characteristics for service %@", service);
    [peripheral discoverCharacteristics:nil forService:service];
    

    当发现特定服务的特性时,peripheral对象会调用代理对象的peripheral:didDiscoverCharacteristicsForService:error:方法。在这个方法中,Core Bluetooth会创建一个CBCharacteristic对象的数组,每个元素表示一个查找到的特性对象。如下代码所示:

    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
    {
        NSLog(@"Discover Characteristics");
        for (CBCharacteristic *characteristic in service.characteristics)
        {
            NSLog(@"Discovered characteristic %@", characteristic);
        }
    }
    

    获取特性的值

    一个特性包含一个单一的值,这个值包含了Peripheral服务的信息。在获取到特性之后,我们就可以从特性中获取这个值。只需要调用CBPeripheral实例的readValueForCharacteristic:方法即可。如下所示:

    NSLog(@"Reading value for characteristic %@", characteristic);
    [peripheral readValueForCharacteristic:characteristic];
    

    当我们读取特性中的值时,peripheral对象会调用代理对象的peripheral:didUpdateValueForCharacteristic:error:方法来获取该值。如果获取成功,我们可以通过特性的value属性来访问它,如下所示:

    - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
    {
        NSData *data = characteristic.value;
    
        NSLog(@"Data = %@", data);
    }
    

    订阅特性的值

    虽然使用readValueForCharacteristic:方法读取特性值对于一些使用场景非常有效,但对于获取改变的值不太有效。对于大多数变动的值来讲,我们需要通过订阅来获取它们。当我们订阅特性的值时,在值改变时,我们会从peripheral对象收到通知。

    我们可以调用CBPeripheral类的setNotifyValue:forCharacteristic:方法来订阅感兴趣的特性的值。如下所示:

    [peripheral setNotifyValue:YES forCharacteristic:characteristic];
    

    当我们尝试订阅特性的值时,会调用peripheral对象的代理对象的peripheral:didUpdateNotificationStateForCharacteristic:error: 方法。如果订阅失败,我们可以实现该代理方法来访问错误,如下所示:

    - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
    {
        ...
    
        if (error)
        {
            NSLog(@"Error changing notification state: %@", [error localizedDescription]);
        }
    }
    

    在成功订阅特性的值后,当特性值改变时,peripheral设备会通知我们的应用。

    写入特性的值

    一些场景下,我们需要写入特性的值。例如我们需要与BLE数字恒温器交互时,可能需要给恒温器提供一个值来设定房间的温度。如果特性的值是可写的,我们可以通过调用CBPeripheral实例的writeValue:forCharacteristic:type:方法来写入值。

    NSData *data = [NSData dataWithBytes:[@"test" UTF8String] length:@"test".length];
    [peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
    

    当尝试写入特性值时,我们需要指定想要执行的写入类型。上例指定了写入类型是CBCharacteristicWriteWithResponse,表示peripheral让我们的应用知道是否写入成功。

    指定写入类型为CBCharacteristicWriteWithResponse的peripheral对象,在响应请求时会调用代理对象的peripheral:didWriteValueForCharacteristic:error:方法。如果写入失败,我们可以在这个方法中处理错误信息。

    小结

    Core Bluetooth框架已经为我们封装了蓝牙通信的底层实现,我们只需要做简单的处理就可以在程序中实现基于蓝牙的通信。不过在游戏中,一般使用Game Kit中自带的蓝牙处理功能,以实现大数据量的通信。Core Bluetooth框架还是比较适合小数据量的通信。

  • 相关阅读:
    网络杂项
    虚拟化
    虚拟化
    ssh
    开机启动命令/服务
    选择表达式
    查询一个表中的重复数据
    oracle逗号分隔函数
    只能为浮点数或整数的正则表达式
    后台模拟弹出上传匡
  • 原文地址:https://www.cnblogs.com/wi100sh/p/5735443.html
Copyright © 2011-2022 走看看