iOS - 蓝牙开发(中心模式)

1.CoreBluetooth.

  iOS蓝牙的相关操作由CoreBluetooth.framework进行管理。核心主要是两种场景:peripheral和central, 可以理解成外设和中心。

  在此主要用到了蓝牙作为中心设备通讯连接硬件的服务。中心模式流程为:

  1.建立中心角色;

  2.扫描外设 (discover);15:25:21

  3.链接外设 (connect);

  4.扫描外设种的服务和特征

    4.1 获取外设的服务 services;

    4.2 获取外设的特征 characteristics;获取特征的descriptor和descriptor的值;

  5.与外设做数据交互,读写数据;

  6.订阅 characteristics 的通知;

  7.断开。

  其中:peripheral,central == 外设和中心,发起连接的时central,被连接的设备为perilheral;

 service and characteristic === 服务和特征 每个设备会提供服务和特征,类似于服务端的api,但是机构不同。每个外设会有很多服务,每个服务中包含很多字段,这些字段的权限一般分为 读read,写write,通知notiy几种,就是我们连接设备后具体需要操作的内容。

2.代码实现

在实际开发过程中,可以根据需求将蓝牙操作写成单例模式,便于整体规划开发。

首先,硬件说明文档中 一般都会给你 三个UUID类似下面这种:

// 设备service UUID
#define BLUETOOTH_SERVICE_UUID                         @"6E400001-B5A3-F393-E0xxx"
// service 下 用来 写数据的 characteristic 的UUID
#define BLUETOOTH_CHARACTERISTIC_WRITE_UUID         @"6E400002-B5A3-F393-E0xxx" 
// service 下 用来 读数据的 characteristic 的UUID ,其实这个读数据一般是在写完之后,拿到写入成功硬件的返回通知,所以其实是通知的uuid
#define BLUETOOTH_CHARACTERISTIC_READ_UUID         @"6E400003-B5A3-F393-E0xxx"

//// characteristic 有properite 属性,其中有对应的 读,写,通知 等权限,不同的权限对应不同的功能。

1.建立中心角色

#import <CoreBluetooth/CoreBluetooth.h>

@interface BlueToothService (<CBPeripheralDelegate,CBCentralManagerDelegate>

//系统蓝牙设备管理对象,可以把他理解为主设备,通过他,可以去扫描和链接外设
@property (nonatomic, strong) CBCentralManager *centerManager;
//保存扫描到的设备
@property (nonatomic, strong) NSMutableArray *peripheralArray;
//当前链接的设备
@property (nonatomic, strong) CBPeripheral *curPeripheral;

 - (void)viewDidLoad {
        [super viewDidLoad];
 //初始化并设置委托和线程队列,最好一个线程的参数可以为nil,默认会就main线程
         manager = [[CBCentralManager alloc]initWithDelegate:self queue:dispatch_get_main_queue()];
}

 

2.扫描外设

 只有当设备蓝牙打开状态下才会扫描外设,开启扫描之前,需要先判断蓝牙状态,这个在centeralManager成功打开的委托中。

/**
 *  判断状态 - 开始扫描
 */
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
   switch (central.state) {
                case CBCentralManagerStateUnknown:
                    NSLog(@">>>CBCentralManagerStateUnknown");
                    break;
                case CBCentralManagerStateResetting:
                    NSLog(@">>>CBCentralManagerStateResetting");
                    break;
                case CBCentralManagerStateUnsupported:
                    NSLog(@">>>CBCentralManagerStateUnsupported");
                    break;
                case CBCentralManagerStateUnauthorized:
                    NSLog(@">>>CBCentralManagerStateUnauthorized");
                    break;
                case CBCentralManagerStatePoweredOff:
                    NSLog(@">>>CBCentralManagerStatePoweredOff");
                    break;
                case CBCentralManagerStatePoweredOn:
                    NSLog(@">>>CBCentralManagerStatePoweredOn");
                    //开始扫描周围的外设
                   [_centerManager scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @NO}];

                    break;
                default:
                    break;
            }
   
}            

 

   其中  1.  CBCentralManagerScanOptionAllowDuplicatesKey值为 No,表示不重复扫描已发现的设备;

       2.实际扫描时,可能只需要扫描指定设备,第一个nil的参数,可以指定 CBUUID 对象的数组,即services。这时扫描只会返回正在广告这些服务的设备。

扫描到的结果在下面方法中显示:

/**
 *  扫描结果
 */
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
    NSLog(@"当扫描到设备:%@",peripheral.name);

}

 1.peripheral:是扫描到的设备,保存在数组中,以备后续链接。

   2.advertisementData: 设备的广播信息。一般硬件厂商会在广播信息中广播一些设备信息,如:mac地址等。可以通过硬件文档解相关数据。

 3.RSSI: 设备信号强度。这个值是波动的,硬件方应该也会给一个相应的计算方法,确定信号强度的具体程度值。

特别:在解析 advertisementData 广播信息的时候,我遇到了一些问题:

        NSData* advData = [dict objectForKey:@"kCBAdvDataManufacturerData"]; 解析出广播信息的data格式,但是这个格式是 NSInlineData,并不是NSData格式的。在各种百度问大佬之后,终于找到了解析 的方法:具体如下:

#pragma mark 将传入的NSData类型转换成NSString并返回

-(NSString *)hexadecimalString:(NSData *)data{
    NSString *result;
    const unsigned char *dataBuffer = (const unsigned char *)[data bytes];
    if (!dataBuffer) {
        return nil;
    }
    NSUInteger dataLength = [data length];
    NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
    for (int i = 0; i<dataLength; i++) {
        //02x 表示两个位置 显示的16进制
        [hexString appendString:[NSString stringWithFormat:@"%02lx",(unsigned long)dataBuffer[i]]];
    }
    result = [NSString stringWithString:hexString];
    
    return result;
    
}

3.链接外设

 //连接方法,扫描到的peripheral 需要保存下来,且必须持有它
 [_centerManager connectPeripheral:peripheral options:nil];
//连接到Peripherals-成功
        - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
        {
            NSLog(@">>>连接到名称为(%@)的设备-成功",peripheral.name);
        }

        //连接到Peripherals-失败
        -(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
        {
            NSLog(@">>>连接到名称为(%@)的设备-失败,原因:%@",[peripheral name],[error localizedDescription]);
        }

        //Peripherals断开连接
        - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
            NSLog(@">>>外设连接断开连接 %@: %@\n", [peripheral name], [error localizedDescription]);

        }

   4.1扫描外设的服务

//发现服务
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
    NSLog(@" service == %@ ---", peripheral.services);
    
    for (CBService *service in peripheral.services) {
        if ([service.UUID isEqual:[CBUUID UUIDWithString:BLUETOOTH_SERVICE_UUID]]) {
            //查找特征
            [self.curPeripheral discoverCharacteristics:nil forService:service];
        }
    }
}

 

 4.2.扫描外设的特征

//发现特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    NSLog(@"chari == %@ ---", service.characteristics);
    
    //write
    for (CBCharacteristic *character in service.characteristics) {
        //写 CBCharacteristic
        if ([character.UUID isEqual:[CBUUID UUIDWithString:BLUETOOTH_CHARACTERISTIC_WRITE_UUID]]) {
            self.writeCharacteristic = character;
        }
        
        // 通知
        if ([character.UUID isEqual:[CBUUID UUIDWithString:BLUETOOTH_CHARACTERISTIC_READ_UUID]]) {
            self.notifyCharacteristic = character;
            [self.curPeripheral setNotifyValue:YES forCharacteristic:character];
        }
    }
    if (_connectedSuccessBlock) {
        _connectedSuccessBlock(peripheral,self.writeCharacteristic,self.notifyCharacteristic);
    }
}

 

  扫描到特征后,可以根据 上文中 提到的 硬件方 给的对应的uuid 保存获取到相应的 CBCharacteristic;通知属性的CBCharacteristic,需要注册通知,才能拿到返回数据。

5.与外设做数据交互

 写数据

//写入数据方法
[self.curPeripheral writeValue:data forCharacteristic:self.writeCharacteristic type:CBCharacteristicWriteWithResponse];

//写成功 返回方法:这个方法 只是表示 此时写入数据成功,但并不是 硬件在写入相应明令之后作出的正确回应,正确返回 是在 通知Characteristic 中返回的
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    NSLog(@"char == %@ -- ", characteristic);
}

 

 //打印出 characteristic 的权限,可以看到有很多种,这是一个NS_OPTIONS,就是可以同时用于好几个值,常见的有read,write,notify,indicate,知知道这几个基本就够用了,前连个是读写权限,后两个都是通知,两种不同的通知方式。
        /*
         typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
         CBCharacteristicPropertyBroadcast                                                = 0x01,
         CBCharacteristicPropertyRead                                                    = 0x02,
         CBCharacteristicPropertyWriteWithoutResponse                                    = 0x04,
         CBCharacteristicPropertyWrite                                                    = 0x08,
         CBCharacteristicPropertyNotify                                                    = 0x10,
         CBCharacteristicPropertyIndicate                                                = 0x20,
         CBCharacteristicPropertyAuthenticatedSignedWrites                                = 0x40,
         CBCharacteristicPropertyExtendedProperties                                        = 0x80,
         CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0)        = 0x100,
         CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0)    = 0x200
         };

         */
        NSLog(@"%lu", (unsigned long)characteristic.properties);


        //只有 characteristic.properties 有write的权限才可以写

 

其实,如果硬件并没有给出相应的uuid,也是可以 根据characteristic 的属性,判断其是 写 还是通知的 特征。

 6.获取通知

  一般情况下,成功写入数据之后,比方蓝牙开锁命令,成功输入命令之后,锁会打开,同时,会在 监听的通知属性下characteristic 中,返回 开锁成功等信息。注册通知就是上文 扫描characteristic方法。如下:

[self.curPeripheral setNotifyValue:YES forCharacteristic:character];

 

  监听通知数据返回方法:

//数据更新回掉
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    NSLog(@"char == %@ -- ", characteristic);
    
    NSData *value = characteristic.value;
}

 

拿到更新后的value 数据后,根据 硬件文档,解析数据,便可得到正确的返回数据。

 7.断开链接

 //停止扫描并断开连接
    -(void)disconnectPeripheral:(CBCentralManager *)centralManager
                     peripheral:(CBPeripheral *)peripheral{
        //停止扫描
        [centralManager stopScan];
        //断开连接
        [centralManager cancelPeripheralConnection:peripheral];
    }

 

 

3.我遇到的问题    

 1.首先我做的是蓝牙锁硬件,总体下来感觉第一最重要的:让硬件方给一个尽可能详细的文档!因为这其中不仅包括数据的解析格式,命令格式,uuid等这些,还有可能会有 加密解密算法,具体的初始密钥或者明文秘文规定等。这些都是很重要的。

2.前面提到的在获取广播数据之后 NSInlineData 数据的解析,其实就是将inlinedata转成 char* 字符格式,接着再转换成oc的字符串。

3.byte 数组。蓝牙命令格式 一般是byte数组,我对这些其实并没有怎么接触过。因为我们是要对命令数据 进行加密 之后再写入。在研究了很久之后,才慢慢理清了具体的操作过程。

 4,一开始在用babybluetooth,确实是走通了,但是总觉得代码太多,因为需求比较简单。如果需求比较复杂,或者可以直接使用babybluetooth,很全面的第三方蓝牙库。

还有,一开始有两个问题没有清楚导致进度特别慢:没有写数据要用 characteristic 的概念,还有不确定写数据 和 写入成功 后的数据返回在哪。在理清楚写 characteristic,和 通知 characteristic的关系后,整个就变得很容易了。

主要是想对使用过的方法和走过的坑进行一下总结,有意见或建议可以联系我QQ804729713。

 

 

 

 


 

posted @ 2018-01-25 16:16  强天  阅读(3068)  评论(0编辑  收藏  举报