蓝牙 BLE CoreBluetooth 初探

蓝牙 BLE CoreBluetooth 初探

蓝牙

Bluetooth 4.0之后就将通讯模式分为高速及低速,低速低耗能简称为BLE,可以连接一些量测型的感测器类型像:心跳计、血压…等,使得iDevice可以不用再使用Dock方式制作产品,也不需要再经过MFi认证才能与iDevice连接,如此一来可以增加APP型态的多元,也能间阶的降低一些成本,如果想要跟BLE周边连接,iOS 5之后提供corebluetooth framework与周边连接,整流程中为DiscoverConnectExploreInteract,下面文章将会从iDevice连线至BLE周边读取资料为例子介绍。

Discover/Connect

依照箭头方向由上而下为顺序依序完成Discover、Connect流程。

CBCentralManager

使用CoreBluetooth Framework中,主要管理连线的是CBCentralManager这个Object,它掌控整个BLE状态的管理,使用时要先对CBCentralManager初始化:

//-----------start-----------
CBCentralManager *CM = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
//------------end------------

现在就开始往下介绍。

centralManagerDidUpdateState

在一开始宣告初CBCentralManager时就有指定Delegate为self,并且必需要在.h内加上Delegate宣告:

//-----------start-----------
@interface TestCoreBluetooth : NSObject<CBCentralManagerDelegate> {
:
:
:
}
//------------end------------

宣告完成后,再加入centralManagerDidUpdateState这个Delegate内容,

//-----------start-----------
-(void)centralManagerDidUpdateState:(CBCentralManager*)cManager
{
    NSMutableString* nsmstring=[NSMutableString stringWithString:@"UpdateState:"];
    BOOL isWork=FALSE;
    switch (cManager.state) {
        case CBCentralManagerStateUnknown:
            [nsmstring appendString:@"Unknown\n"];
            break;
        case CBCentralManagerStateUnsupported:
            [nsmstring appendString:@"Unsupported\n"];
            break;
        case CBCentralManagerStateUnauthorized:
            [nsmstring appendString:@"Unauthorized\n"];
            break;
        case CBCentralManagerStateResetting:
            [nsmstring appendString:@"Resetting\n"];
            break;
        case CBCentralManagerStatePoweredOff:
            [nsmstring appendString:@"PoweredOff\n"];
            if (connectedPeripheral!=NULL){
                [CM cancelPeripheralConnection:connectedPeripheral];
            }
            break;
        case CBCentralManagerStatePoweredOn:
            [nsmstring appendString:@"PoweredOn\n"];
            isWork=TRUE;
            break;
        default:
            [nsmstring appendString:@"none\n"]; 
            break;
    }
    NSLog(@"%@",nsmstring);
    [delegate didUpdateState:isWork message:nsmstring getStatus:cManager.state];
}
//------------end------------

centralManagerDidUpdateState的Delegate是用来得知蓝牙目前的状态,所以会有个结果是用来判断iDevice是否支援BLE,因为BLE是在iphone 4s、New iPad之后才有的,现阶段还是需要侦测使用的环境,当然可以根据这些状态的口报来决定APP的功能或其他提示使用者的动作。

scanForPeripheralsWithServices

先前确定周边支援BLE且运作正常后,我们就要来开启BLE搜寻功能来寻找BLE的周边,当周边接收到搜寻功能的广播讯息时,依照BLE通讯规范,周边会在一定时间内回复,所以我们在此可以设定2秒的Timer计时器,当时间一到就停止scan的功能。

//-----------start-----------
CBCentralManager *CM = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
[CM scanForPeripheralsWithServices:nil options:options];
[NSTimer scheduledTimerWithTimeInterval:2.0f target:self selector:@selector(scanTimeout:) userInfo:nil repeats:NO];
//------------end------------

设定2秒后触发执行scanTimeoutmethod,再将scanForPeripheralsWithServices的值设为nil,代表搜寻的Service type不受限制,当你搜寻特定时,就必需要将它的UUID填入,像范例这样:

//-----------start-----------
 NSArray    *uuidArray= [NSArray arrayWithObjects:[CBUUID UUIDWithString:@"180D"], nil];
        [CM scanForPeripheralsWithServices:uuidArray options:options];
//------------end------------

其中UUIDWithString:@"180D"180D就是Heart Rate Service type,一旦指定Service type,结果就只会将周边有Heart Rate类型一一列出来,要了解更多的Service Type可以到Bluetooth官网查询。 当您了解Service type是哪一种类型时就可以来做对应的流程及资料的解析,也可以制作出符合一般标准周边的通用APP。

didDiscoverPeripheral

didDiscoverPeripheral属于Delegate功能,所以要按照它预设的宣告将要处理的过程写在里面,格式如下:

//-----------start-----------
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{

//处理过程

}
//------------end------------

advertisementData会报告可以连线的周边内容, 如果将它印出来会像这样:

//-----------start-----------
adverisement:{
    kCBAdvDataLocalName = "INFOS 4090v35.05";
    kCBAdvDataServiceUUIDs =     (
        "Unknown (<fff0>)"
    );
    kCBAdvDataTxPowerLevel = 0;
}

//------------end------------

RSSI是讯号的强度,是以NSNumber Object存在,取得后可以依照NSNumber的方式整数值做处理与转换,接下来我们将一些资讯列印出来,整个范例可以是这样子:

//-----------start-----------

-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { NSMutableString* nsmstring=[NSMutableString stringWithString:@"\n"]; [nsmstring appendString:@"Peripheral Info:"]; [nsmstring appendFormat:@"NAME: %@\n",peripheral.name]; [nsmstring appendFormat:@"RSSI: %@\n",RSSI]; if (peripheral.isConnected){ [nsmstring appendString:@"isConnected: connected"]; }else{ [nsmstring appendString:@"isConnected: disconnected"]; } NSLog(@"adverisement:%@",advertisementData); [nsmstring appendFormat:@"adverisement:%@",advertisementData]; [nsmstring appendString:@"didDiscoverPeripheral\n"]; NSLog(@"%@",nsmstring); } //------------end------------

结果输出:

//-----------start-----------
2013-02-25 14:43:17.243 gw-health-01[141:907] 
Peripheral Info:NAME: INFOS 4090v35.05
RSSI: -69
isConnected: disconnected
adverisement:{
    kCBAdvDataServiceUUIDs =     (
        "Unknown (<fff0>)"
    );
}
//------------end------------

如果有发现可连线的BLE周边,它就会不断的执行didDiscoverPeripheral,并将资讯传入,利用这个方式将每次得到的结果存入Array,就可以得到搜寻周边的结果然后再提供给USER选择,或是从中可以去判断某个特别的周边是否存在而决定要不要连线。

stopScan

执行scanForPeripheralsWithServices 扫描周边设定2秒的Timer,当时间到时就停止scan,一般2秒内无反应就可以当作是没有其他周边回应,承上面scanForPeripheralsWithServices中有设定Timer去呼叫scanTimeout,所以将stopScan写在scanTimeout里面:

//-----------start-----------
- (void) scanTimeout:(NSTimer*)timer
{
    if (CM!=NULL){
            [CM stopScan];
    }else{
        NSLog(@"CM is Null!");
    }
    NSLog(@"scanTimeout");
}

//------------end------------

connectPeripheral

didDiscoverPeripheral得到的BLE周边列表让User选择要连线的BLE,再将 CBPeripheral传入connectPeripheral进行连线,格式:

//-----------start-----------
 [CBCentralManager connectPeripheral:CBPeripheral* options:NSDictionary*]
//------------end------------

在此将它包装成一个connect Method,

//-----------start-----------
- (void) connect:(CBPeripheral*)peripheral
{

    if (![peripheral isConnected]) {
        [CM connectPeripheral:peripheral options:nil];
        connectedPeripheral=peripheral;
    }

}

//------------end------------

option传入nil,connectPeripheral传入Method connect的值。

didConnectPeripheral

执行connectPeripheral之后并连线成功后就会引发didConnectPeripheral的Delegate:

//-----------start-----------
-(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
:
:
:

}
//------------end------------

在这里有个重点,连线成功后引发Delegate时,就必需要针对其CBPeripheral马上进行discoverServices的动作,去了解周边提供什么样的Services

执行discoverServices之后又会引发另一个didDiscoverServicesDelegate,不过这会在Explore中介绍。

Explore

Discover/Connect 中使用CBCentralManager进行连线/搜寻BLE周边的功能,连线之后需要靠的是CBPeripheral来传送/接收资料。

CBPeripheral

//-----------start-----------
@interface DYCoreBluetooth : NSObject<CBCentralManagerDelegate, CBPeripheralDelegate> {

:
:
:

}

//------------end------------

之后连线的重点全都是在Delegate的互动,查看Service Type或是有什么样的Services可以提供。

didConnectPeripheral

前面有稍为介绍didConnectPeripheral,这是在连线成功后就会引发的Delegate,但一定要在这里执行一些Method才可以顺利的引发另一个CBPeripheral的Delegate去查看有什么样的Services

//-----------start-----------
-(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"Connect To Peripheral with name: %@\nwith UUID:%@\n",peripheral.name,CFUUIDCreateString(NULL, peripheral.UUID));

    peripheral.delegate=self;
    [peripheral discoverServices:nil];//一定要执行"discoverService"功能去寻找可用的Service

}

//------------end------------

例子中已经将peripheral.delegate=self,接下来进行peripheral的任何动做引发的Delegate都在这个Object中,执行discoverServicesMethod,让它去寻找Services,一找到Services就又会引发didDiscoverServicesDelegate,这样我们就可以了解有什么Services。

didDiscoverServices

从这里开始就是最关键

//-----------start-----------
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
    NSLog(@"didDiscoverServices:\n");
    if( peripheral.UUID == NULL  ) return; // zach ios6 added
    if (!error) {
        NSLog(@"====%@\n",peripheral.name);
        NSLog(@"=========== %d of service for UUID %@ ===========\n",peripheral.services.count,CFUUIDCreateString(NULL,peripheral.UUID));

        for (CBService *p in peripheral.services){
            NSLog(@"Service found with UUID: %@\n", p.UUID);
            [peripheral discoverCharacteristics:nil forService:p];
        }

    }
    else {
        NSLog(@"Service discovery was unsuccessfull !\n");
    }

}

//------------end------------

peripheral.services.count会知道有多少个Services,在每个Servces中还会有Characteristics需要了解,所以会针对每个Service来执行 peripheral discoverCharacteristics: forService:去知道每个Service下有多少个Characteristics提供传送/接收的沟通,在执行discoverCharacteristics后也引发didDiscoverCharacteristicsForService Delegate,最后再由didDiscoverCharacteristicsForService真正的判断什么样的Service、什么样的Characteristic再进行之后收到的资料的处理动作,例如: 发现2A37的Characteristic,就要进行注册通知,到时候BLE周边发讯息过来才会立即的得到通知并得到资料。

didDiscoverCharacteristicsForService

整个最关键的地方就是这个Delegate,程式架构如下:

//-----------start-----------
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{

:
:
:

}

//------------end------------

Interact

完成didDiscoverCharacteristicsForService之后,整个连线过程算是完成,之后的didUpdateValueForCharacteristicDelegate是整个资料接收动作都在这发生,经过接收到的资料进行即时处理就可以取得BLE周边的讯息,如果必需要将资料传至BLE周边时,再使用writeValue的Method将资料出,以上为BLE连线最基本使用方式就大致上完成。

didDiscoverCharacteristicsForService

由Apple提供的资料撷取某部分来了解架构,等下程式就是利用这架构去寻访所有的CharacteristicsForService

每个Servic下都会有很多的Characteristics,Characteristics是提供资料传递的重点,它会有个UUID编号,再由这个编号去Bluetooth 官方查表得到是哪种资料格式,知道格式后就能去将资料解开并加以使用。

真正的例子:

//-----------start-----------
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{

    CBService *s = [peripheral.services objectAtIndex:(peripheral.services.count - 1)];
    NSLog(@"=========== Service UUID %s ===========\n",[self CBUUIDToString:service.UUID]);
    if (!error) {
        NSLog(@"=========== %d Characteristics of service ",service.characteristics.count);

        for(CBCharacteristic *c in service.characteristics){

            NSLog(@" %s \n",[ self CBUUIDToString:c.UUID]);
          //  CBService *s = [peripheral.services objectAtIndex:(peripheral.services.count - 1)];
            if(service.UUID == NULL || s.UUID == NULL) return; // zach ios6 added


            //Register notification
            if ([service.UUID isEqual:[CBUUID UUIDWithString:@"180D"]])

            {
                if ([c.UUID isEqual:[CBUUID UUIDWithString:@"2A37"]])
                {
                    [self notification:service.UUID characteristicUUID:c.UUID peripheral:peripheral on:YES];
                    NSLog(@"registered notification 2A37");
                }
                if ([c.UUID isEqual:[CBUUID UUIDWithString:@"2A38"]])
                {
                    [self notification:service.UUID characteristicUUID:c.UUID peripheral:peripheral on:YES];
                    NSLog(@"registered notification 2A38");
                }
                if ([c.UUID isEqual:[CBUUID UUIDWithString:@"2A39"]])
                {
                    [self notification:service.UUID characteristicUUID:c.UUID peripheral:peripheral on:YES];
                    NSLog(@"registered notification 2A39");
                }

            }

        }
        NSLog(@"=== Finished set notification ===\n");


    }
    else {
        NSLog(@"Characteristic discorvery unsuccessfull !\n");

    }
    if([self compareCBUUID:service.UUID UUID2:s.UUID]) {//利用此来确定整个流程都结束后才能设定通知
        [delegate didConnected:peripheral error:error];
        NSLog(@"=== Finished discovering characteristics ===\n");
        //全部服务都读取完毕时才能使用!

    }

}

//------------end------------

上面 例子是以Heart Rate(180D)为主,

Heart Rate的规格来说,0x2A37可以得到心跳的数据,所以针对此项进行注册通知,一旦有新的数据就会传入新的数据资料并呼叫didUpdateValueForCharacteristicDelegate,来得到每次的心跳数据更新。

//-----------start-----------
 [(CBPeripheral *)p setNotifyValue:(BOOL) forCharacteristic:CBCharacteristic *)]

//------------end------------

将Characteristic的Point传入并设定setNotifyValue:on就完成注册通知,之后如果有更新资料时就会引发didUpdateValueForCharacteristic Delegate,再进行资料处理。

notification

在设定注册通知过程有点繁杂,所以我自行撰写一个Method为notification,它可以从Service UUID及Characteristic UUID来找到Service与Characteristic的Object Point:。

//-----------start-----------
-(void) notification:(CBUUID *) serviceUUID characteristicUUID:(CBUUID *)characteristicUUID peripheral:(CBPeripheral *)p on:(BOOL)on {

    CBService *service = [self getServiceFromUUID:serviceUUID p:p];
    if (!service) {
        if (p.UUID == NULL) return; // zach ios6 addedche
        NSLog(@"Could not find service with UUID on peripheral with UUID \n");
        return;
    }
    CBCharacteristic *characteristic = [self getCharacteristicFromUUID:characteristicUUID service:service];
    if (!characteristic) {
        if (p.UUID == NULL) return; // zach ios6 added
        NSLog(@"Could not find characteristic with UUID  on service with UUID  on peripheral with UUID\n");
        return;
    }
    [p setNotifyValue:on forCharacteristic:characteristic];

}


-(CBService *) getServiceFromUUID:(CBUUID *)UUID p:(CBPeripheral *)p {

    for (CBService* s in p.services){
        if ([self compareCBUUID:s.UUID UUID2:UUID]) return s;
    }
    return nil; //Service not found on this peripheral
}

-(CBCharacteristic *) getCharacteristicFromUUID:(CBUUID *)UUID service:(CBService*)service {

    for (CBCharacteristic* c in service.characteristics){
        if ([self compareCBUUID:c.UUID UUID2:UUID]) return c;
    }
    return nil; //Characteristic not found on this service
}
//------------end------------

使用时只需要将Service/Characteristic的UUID及得到周边的Peripheral物件传入,并设定是on(YES)或off(NO)就完成。

didUpdateValueForCharacteristic

didUpdateValueForCharacteristic在连线完成后对于数据资得的取得显的非常重要,范例中有比对2个UUID为2A372A38

//-----------start-----------
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{

    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A37"]])
    {
        if( (characteristic.value)  || !error )
        {

        }
    }

    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A38"]])
    {
        //set refresh int
        uint8_t val = 1;
        NSData* valData = [NSData dataWithBytes:(void*)&val length:sizeof(val)];
        [peripheral writeValue:valData forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
    }

}

//------------end------------

针对这两个UUID成立时在这个Delegate做对应处理的工作,范例中以2A38来解说一下:

当更新资料为2A38时,程式将直接写入 1 ,为什么写入1呢?在下表中可以了解,1所代表的就是Chest

意思是告诉心跳感测器量测的位置是在胸部的部分。

注意

整个didUpdateValueForCharacteristic在处理时请注意资料格式的解释,往往是因为格式解释错误才会得到不正确的资料。

延伸阅读

目前已经完成CoreBluetooth For Centeral系列文章,下面为文章的标题:

CoreBluetooth for Central (1) ~ (7)

这是讲述CoreBluetooth For Central使用方式,整篇文章最后会完成Heart Rate Measurement连接Polar H7的例子结束Central的学习。

1.Bluetooth Low Energy(BLE)

2.BLE Status

3.Discover BLE Device

4.Connect BLE Device

5.ReConnect BLE Device

6.Explore Services from Device

7.Interact Data

8,Read RSSI

更新资讯

日期 内容
2015/01/27 增加延伸阅读