iOS开发硬件交互-蓝牙模式

时间:2024-03-11 17:55:22

关于

  近期都在做一些与智能硬件交互的项目,从公司的项目走向,可以明显的感觉到越来越多的家电,医疗器械,家居用品公司开始借助手机APP来帮助他们实现自家产品的“智能化”,优化用户体验。相信随着研发技术的提升和研发成本的降低,这种智能软硬件结合的产品将会迅速普及开来。

从目前APP同硬件模块通信的方式来看无非分为以下几种:

  • 蓝牙连接模式
  • WiFi 连接模式(Socket 或 HTTP server)
  • DLNA 音视频共享 (iOS端还可使用 AirPlay)

这篇随笔是对近期项目技术点的一个简单的回顾总结。


蓝牙连接模式

蓝牙基础知识

  • iOS平台下我们可以使用 MFI(ExternalAccessory 框架) 或 BLE (CoreBluetooth 框架) 进行蓝牙开发,但实际中我们基本使用 CoreBluetooth 框架,因为它功能更强大,支持蓝牙4.0标准。

  • CoreBluetooth 框架的核心是 peripheral 和 central, 可以理解成外设和中心,发起连接的是 central,被连接的设备为 peripheral,它们是一组相对概念。比如,当手机去连接控制蓝牙耳机时,你的手机就是 central,当手机蓝牙被另一个手机连接并为其提供服务时就是 peripheral。

  • CoreBluetooth 框架分别为“中心模式”和“外设模式”提供一组API。在移动端开发中,我们通常用到的中心模式。

  • Service 和 Characteristic:蓝牙设备通过GATT协议定义的数据通讯方式。一个 peripheral 可以可以提供多种 Service,一种 Service 又可以包含多个不同的 Characteristic。central 通过 peripheral 的 Characteristic 来读写外设的数据,和获取通知。

蓝牙的两种工作模式

1、中心模式
1. 建立中心
2. 扫描外设(discover)
3. 连接外设(connect)
4. 扫描外设中的服务和特征(discover)
    - 4.1 获取外设的 services
    - 4.2 获取外设的 Characteristics,获取Characteristics的值,获 Characteristics的 Descriptor 和 Descriptor 的值
5. 与外设做数据交互(explore and interact)
6. 订阅 Characteristic 的通知
7. 断开连接(disconnect)
2、外设模式
1. 启动一个 Peripheral 管理对象
2. 本地 Peripheral 设置服务,特性,描述,权限等等
3. Peripheral 发送广播
4. 设置处理订阅、取消订阅、读 characteristic、写 characteristic 的委托方法
3.蓝牙设备的工作状态
1. 准备(standby)
2. 广播(advertising)
3. 监听扫描(Scanning
4. 发起连接(Initiating)
5. 已连接(Connected)

代码演示


#import "ViewController.h"
#import <CoreBluetooth/CoreBluetooth.h>

@interface ViewController ()<CBCentralManagerDelegate,CBPeripheralDelegate>
{
    CBCentralManager *_centralManager; //central 管理器 (中心模式)
    NSMutableArray   *_peripherals; //peripheral 外设集
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    _peripherals = [NSMutableArray array];
    
    //初始化并设置委托和线程队列
    _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue() options:nil];
}


#pragma mark - CBCentralManagerDelegate
// _centralManager 初始化状态
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
    NSLog(@"%s -- %ld", __func__ ,(long)central.state);
    
//    typedef NS_ENUM(NSInteger, CBCentralManagerState) {
//        CBCentralManagerStateUnknown = 0,
//        CBCentralManagerStateResetting,
//        CBCentralManagerStateUnsupported,
//        CBCentralManagerStateUnauthorized,
//        CBCentralManagerStatePoweredOff,
//        CBCentralManagerStatePoweredOn,
//    };
    
    //手机蓝牙状态判断
    switch (central.state) {
        case CBCentralManagerStateUnknown:
            NSLog(@"central state >> CBCentralManagerStateUnknown");
            break;
        case CBCentralManagerStateResetting:
            NSLog(@"central state >> CBCentralManagerStateResetting");
            break;
        case CBCentralManagerStateUnsupported:
            NSLog(@"central state >> CBCentralManagerStateUnsupported");
            break;
        case CBCentralManagerStateUnauthorized:
            NSLog(@"central state >> CBCentralManagerStateUnauthorized");
            break;
        case CBCentralManagerStatePoweredOff:
            NSLog(@"central state >> CBCentralManagerStatePoweredOff");
            break;
        case CBCentralManagerStatePoweredOn:
        {
            NSLog(@"central state >> CBCentralManagerStatePoweredOn");
            //开始扫描 peripheral
            //if <i>serviceUUIDs</i> is <i>nil</i> all discovered peripherals will be returned.
            [_centralManager scanForPeripheralsWithServices:nil options:nil];
            break;
        }
        default:
            break;
    }
}

// 发现蓝牙外设 peripheral
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI{
//    NSLog(@"%s -- %@ RRSI:%ld", __func__ ,peripheral.name,(long)RSSI.integerValue);
    
    //连接所有 “Bioland” 名前缀的 peripheral
    if ([peripheral.name hasPrefix:@"Bioland-BGM"]) {
        //一个主设备最多能连7个外设,但外设最多只能给一个主设备连接,连接成功,失败,断开会进入各自的委托
        
        //找到的设备必须持有它,否则CBCentralManager中也不会保存peripheral,那么CBPeripheralDelegate中的方法也不会被调用!
        [_peripherals addObject:peripheral];
        [_centralManager connectPeripheral:peripheral options:nil];
    }
}

// 中心设备(central) 连接到 外设(peripheral)
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
    NSLog(@"连接到 外设(peripheral) -- %@" ,peripheral.name);
    
    //设置的peripheral委托CBPeripheralDelegate
    [peripheral setDelegate:self];
    //扫描外设Services,成功后会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
    [peripheral discoverServices:nil];
}

// 外设连接失败
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
    NSLog(@"%s -- %@", __func__ ,peripheral.name);

}

// peripheral 断开连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
    NSLog(@"%s -- %@", __func__ ,peripheral.name);

}




#pragma mark - CBPeripheralDelegate
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
      NSLog(@"扫描到服务:%@",peripheral.services);
    if (error)
    {
        NSLog(@"Discovered services for %@ with error: %@", peripheral.name, [error localizedDescription]);
        return;
    }
    
    for (CBService *service in peripheral.services) {
        NSLog(@"%@",service.UUID);
        if ([service.UUID.UUIDString isEqualToString:@"1000"]) {
        //扫描每个service的Characteristics,扫描到后会进入方法: -(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
        [peripheral discoverCharacteristics:nil forService:service];
        }
    }
}

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
    if (error)
    {
        NSLog(@"error Discovered characteristics for %@ with error: %@", service.UUID, [error localizedDescription]);
        return;
    }
    
    for (CBCharacteristic *characteristic in service.characteristics)
    {
        NSLog(@"service:%@ 的 Characteristic: %@",service.UUID,characteristic.UUID);
    }
    
    //获取Characteristic的值,读到数据会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
    for (CBCharacteristic *characteristic in service.characteristics){
        {
            [peripheral readValueForCharacteristic:characteristic];
        }
    }
    
//    //搜索Characteristic的Descriptors,读到数据会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
//    for (CBCharacteristic *characteristic in service.characteristics){
//        [peripheral discoverDescriptorsForCharacteristic:characteristic];
//    }
}

//获取的charateristic的值
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    //打印出characteristic的UUID和值
    //!注意,value的类型是NSData,具体开发时,会根据外设协议制定的方式去解析数据
    NSLog(@"characteristic uuid:%@  value:%@",characteristic.UUID.UUIDString,characteristic.value);
   
    if ([characteristic.UUID.UUIDString isEqualToString:@"1001"]) {
        
        //Bioland-BGM 05应答包 测试数据
        char byte[11];
        memset(byte, 0, 11);
        byte[0] = 0x5a;
        byte[1] = 0x0b;
        byte[2] = 0x05;
        byte[3] = 0x0e;
        byte[4] = 0x0b;
        byte[5] = 0x08;
        byte[6] = 0x0c;
        byte[7] = 0x12;
        byte[8] = 0xa9;
        byte[9] = 0x00;
        byte[10] = 0x00;
        
        [self writeCharacteristic:peripheral characteristic:characteristic value:[NSData dataWithBytes:byte length:11]];
    }
    
    
    if ([characteristic.UUID.UUIDString isEqualToString:@"1002"]) {
        [self notifyPeripheral:peripheral characteristic:characteristic];
    }
    
//    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"00001002-0000-1000-8000-00805f9b34fb"]]) {
//        [self notifyPeripheral:peripheral characteristic:characteristic];
//    }
}


////搜索到Characteristic的Descriptors
//-(void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
//    
//    //打印出Characteristic和他的Descriptors
//    NSLog(@"characteristic uuid:%@",characteristic.UUID);
//    for (CBDescriptor *d in characteristic.descriptors) {
//        NSLog(@"Descriptor uuid:%@",d.UUID);
//    }
//    
//}
////获取到Descriptors的值
//-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error{
//    //打印出DescriptorsUUID 和value
//    //这个descriptor都是对于characteristic的描述,一般都是字符串,所以这里我们转换成字符串去解析
//    NSLog(@"characteristic uuid:%@  value:%@",[NSString stringWithFormat:@"%@",descriptor.UUID],descriptor.value);
//}



//写数据
-(void)writeCharacteristic:(CBPeripheral *)peripheral
            characteristic:(CBCharacteristic *)characteristic
                     value:(NSData *)value{
    
    //打印出 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(@"write %lu", (unsigned long)characteristic.properties);
    
    //只有 characteristic.properties 有write的权限才可以写
    if(characteristic.properties & CBCharacteristicPropertyWrite){
        /*
         最好一个type参数可以为CBCharacteristicWriteWithResponse或type:CBCharacteristicWriteWithResponse,区别是是否会有反馈
         */
        [peripheral writeValue:value forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
    }else{
        NSLog(@"该字段不可写!");
    }
}



//设置通知
-(void)notifyPeripheral:(CBPeripheral *)peripheral
             characteristic:(CBCharacteristic *)characteristic{
    NSLog(@"Notify %lu", (unsigned long)characteristic.properties);

    //设置通知,数据通知会进入:didUpdateValueForCharacteristic方法
    [peripheral setNotifyValue:YES forCharacteristic:characteristic];
}

//取消通知
-(void)cancelNotifyPeripheral:(CBPeripheral *)peripheral
                   characteristic:(CBCharacteristic *)characteristic{
    
    [peripheral setNotifyValue:NO forCharacteristic:characteristic];
}

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

以上代码基于 《CoreBluetooth》 实现蓝牙简单交互,所有操作均在其代理方法中实现,过程十分繁琐,这里推荐一个蓝牙开源库:BabyBluetooth