蓝牙 BLE CoreBluetooth 初探
蓝牙
Bluetooth 4.0之后就将通讯模式分为高速及低速,低速低耗能简称为BLE,可以连接一些量测型的感测器类型像:心跳计、血压…等,使得iDevice可以不用再使用Dock方式制作产品,也不需要再经过MFi认证才能与iDevice连接,如此一来可以增加APP型态的多元,也能间阶的降低一些成本,如果想要跟BLE周边连接,iOS 5之后提供corebluetooth framework与周边连接,整流程中为Discover、Connect、Explore、Interact,下面文章将会从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秒后触发执行scanTimeout
method,再将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之后又会引发另一个didDiscoverServices
Delegate,不过这会在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中,执行discoverServices
Method,让它去寻找Services,一找到Services就又会引发didDiscoverServices
Delegate,这样我们就可以了解有什么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
之后,整个连线过程算是完成,之后的didUpdateValueForCharacteristic
Delegate是整个资料接收动作都在这发生,经过接收到的资料进行即时处理就可以取得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
可以得到心跳的数据,所以针对此项进行注册通知,一旦有新的数据就会传入新的数据资料并呼叫didUpdateValueForCharacteristic
Delegate,来得到每次的心跳数据更新。
//-----------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为2A37与2A38
//-----------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的学习。
6.Explore Services from Device
更新资讯
日期 | 内容 |
---|---|
2015/01/27 | 增加延伸阅读 |