iOS 蓝牙开发 CoreBluetooth 应用

iOS CoreBluetooth 应用学习

目前iOS的蓝牙应用主要应用在穿戴、音箱、耳机短距离传输等领域,应用场景非常广阔。而目前对于开发者来说,应用较多的只有BLE4.0,因为苹果的2.0蓝牙是需要MFI(make for iphone)验证的,而厂商的利润本来就非常低了,还得搞个MFI认证的话就不赚钱了。所以本篇文章只是学习蓝牙4.0的开发应用。

本篇主要想从两方面来分析和学习CoreBluetooth框架,让iPhone 分别作为蓝牙外设 和作为蓝牙中央。首先我们需要先导入CoreBluetooth.framework。然后我们先来看看iOS蓝牙使用的流程,调用规则是下图这样的:

CoreBluetooth Image

底层的GATT和ATT、L2CAP我们是不直接调用的,感兴趣的同学可以进一步学习一下GATT和L2CAP,这两个在嵌入式用得比较多。我们主要是使用CoreBluetooth提供的方法来进行与设备的交互。

手机作为蓝牙中央

通常这种情况会多一些,因为手机具备了强大的运算能力和出色的表达能力,应用领域可以参考运动手环、心率测试仪、血压计等等。也就是下面这种情况:

工作图

但是这种情况下,如果以网络的模型来看,此刻的iOS设备是作为客户端的,而蓝牙设备就作为服务器端。这是因为蓝牙外设时需要建立一个蓝牙通信,设定好服务和特征值、描述数据等信息,然后广播到空气中,iPhone通过服务搜索,发现设备,才进行连接的。

设备广播

我们先来看看外设所能提供的服务信息,这些信息是可以用来标记数据交互、或者作为通道交互数据,其实它的数据模型和网络是非常类似的,可以理解为建立了多个通道的即时通讯。

外设架构

蓝牙外设段可以理解为能提供以下字段作为传输通道

1、服务 Service

1.1 特征值 Characteristic

    1.1.1 描述符 

1.2 特征值  Characteristic

    1.2.1 描述符

2、服务 Service

2.1 特征值 Characteristic

    2.1.1 描述符

2.2 特征值 Characteristic

    2.2.1 描述符

当iPhone作为中心的时候,主要用到的类库有两个:CBCentralManager(外围设备管理器)、CBPeripheral(远端外围设备)

CBCentralManager 介绍

iPhone是通过这个类来进行蓝牙设备的发现、管理、连接、异常处理。
初始化:

centralMgr = CBCentralManager.init(delegate: self, queue: DispatchQueue.main, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true,CBConnectPeripheralOptionNotifyOnNotificationKey:true])
/* 参数说明
  CBCentralManagerOptionShowPowerAlertKey
  填一个Bool值,用来指定如果蓝牙设备断电的时候,系统是否会发出警告
  CBCentralManagerOptionRestoreIdentifierKey
  填一个String作为唯一标记
*/

扫描外设:

centralMgr.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
/** 参数说明
  CBCentralManagerScanOptionAllowDuplicatesKey
  一个布尔值,指定是否应在没有重复过滤的情况下运行扫描。
  
  CBCentralManagerScanOptionSolicitedServiceUUIDsKey
  指定服务UUID的数组扫描特定设备
*/

停止扫描:

centralMgr.stopScan() 

连接设备:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = itemArray[indexPath.row]
        cbPeripheral = item
        centralMgr.connect(item, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true])
        tableView.deselectRow(at: indexPath, animated: true)
    }
/** 特别说明一下连接的可选参数
  CBConnectPeripheralOptionNotifyOnConnectionKey
  填一个Bool值,指定后台连接外围设备时,是否告知系统,并弹窗提示
  CBConnectPeripheralOptionNotifyOnDisconnectionKey
  填一个Bool值,指定后台断开外围设备时,是否告知系统,并弹窗提示
  CBConnectPeripheralOptionNotifyOnNotificationKey
  填一个Bool值,指定系统是否对外围发过来的每一个通知都弹窗提示
  CBConnectPeripheralOptionEnabTransportBridgeingKey
  如果已经通过低功耗蓝牙连接,则可以桥接经典蓝牙的配置文件(GATT)
  CBConnectPeripheralOptionRequiresANCS
  填一个Bool值,设定连接设备时是否需要连接(ANCS)服务,接收推送服务
  CBConnectPeripheralOptionStarDelayKey
  填一个Bool,设置系统连接前是否要延迟
*/

CBCentralManager是通过接收代理的方式来获得设备的连接状态以及信息变动,所以可以参考以下的代理消息设置:

/// 连接上了
    /// - Parameters:
    ///   - central: centerMgr
    ///   - peripheral: 蓝牙外设
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
      //连接上后,选择跳到另一个界面去进行更多的通讯交互
      //    performSegue(withIdentifier: "peripheralSegue", sender: peripheral)
    }
    
    /// 断开连接
    /// - Parameters:
    ///   - central: centerMgr
    ///   - peripheral: 蓝牙外设
    ///   - error: 错误原因
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        
    }
​
    ///ANCS授权状态回调
    func centralManager(_ central: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral) {
        
    }
   
    
    /// 发现蓝牙设备回调
    /// - Parameters:
    ///   - central: centerMgr
    ///   - peripheral: 蓝牙外设
    ///   - advertisementData: 描述
    ///   - RSSI: 信号强度
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        if !itemArray.contains(peripheral) && (peripheral.name != nil){
            itemArray.append(peripheral)
            listTable.reloadData()
        }
    }
    
    //断开回连
    func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
        
        centralMgr.connect(cbPeripheral, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true])
        
    }

CBPeripheral 蓝牙设备的交互

其实这个CBPeripheral就是俄罗斯套娃的结构,一层环节一层,先去获取了服务,然后根据服务获取服务的特征值、描述符;

发现服务:

/*
1、discoverServices 发现外围设备的指定服务
2、discoverIncludedServices([CBUUID]?,for:CBService) 发现先前发现的服务中所包含的服务
3、services:[CBService]? 外围设备已发现的服务列表
*/
mainPeripheral.discoverServices(nil)

发现服务回调:

 //服务发现回调
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        for service in peripheral.services! {
            //根据设备回调的服务,再去请求特征值
            peripheral.discoverCharacteristics(nil, for: service)
            print(service.uuid.uuidString,"services")
        }
    }

设备特征值回调:

 //设备特征回调
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        for character in service.characteristics! {
            print(character.uuid.uuidString,"characteristics")
            peripheral.setNotifyValue(true, for: character)//订阅所有特征值的通知
            peripheral.readValue(for: character)//读取所有特征值
            peripheral.discoverDescriptors(for: character)//读取所有特征值的描述符
        }
        peripheralTable.reloadData()
    }

描述符回调:

 //描述符回调
    func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
        for descriptor in characteristic.descriptors! {
            print(descriptor.uuid.uuidString,"descriptors")
        }
        peripheralTable.reloadData()
    }

建立通讯之后,需要对订阅的值或者通道进行监听,监听如下:

 //更新特征值通知
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        print("\(characteristic.uuid.uuidString): \(characteristic.uuid.data)")
    }
    //设备值更新通知
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if (characteristic.value != nil) {
            let k = String.init(data: characteristic.value!, encoding: String.Encoding.utf8)
                  print("\(characteristic.uuid.uuidString): \(k)")//收到来自通知的数据
        }
         
    }

还有其他一些状态变更,可参考下面:

//设备写入成功回调
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        print("characteristic is writed!")
    }
    
    //服务名更改
    func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
        
    }
    //外设名字变更
    func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
        
    }

读取特征值和描述符:

readValue(for:CBCharacteristic) 读取指定特征值

readValue(for:CBDescriptor) 读取指定特征描述符的值

写入特征值和描述符:

writeValue(Data,for:CBCharacteristic,type:CBCharacteristicWriteType)写入特征值

writeValue(Data,for:CBDescriptor)写入特征描述符的值

maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int可以通过单个写入类型发送到特征的最大数据量(以字节为单位)。CBCharacteristicWriteType 表示可能写入特征值的类型的值,withResponse 写入值成功时有返回;withoutResopnse 写入值成功时不设返回值。

手机作为蓝牙外设

在实际的应用场景中,这种情况会比较少需要用上,可能会用于手机APP之间的小数据交互等等。当手机作为外设的时候,要用到的类是:CBPeripheralManagerCBCharacteristic

手机作为外设

作为外设意味着,需要为Central提供Service、Characteristic、Descriptor,同样的,我们也需要造一个俄罗斯套娃出来,一层一层套起来。

初始化:

phermgr = CBPeripheralManager.init(delegate: self, queue: nil)

当收到了蓝牙状态监测到成功打开的时候,需要为它添加服务,开始套娃

let serviceUUID1 = "EE00"
let notifyCharacteristicUUID = "EE01"
let readCharacteristicUUID =  "EE02"
let writeCharacteristicUUID = "EE03"
let LocalNameKey = "Gt_0"
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        switch peripheral.state {
        case CBManagerState.poweredOn:  //蓝牙已打开正常
            NSLog("启动成功,开始搜索")
             //不限制
            let cbu = CBUUID.init(string:CBUUIDCharacteristicUserDescriptionString)
            
            //服务
            let service1 = CBMutableService.init(type: CBUUID.init(string: serviceUUID1), primary: true)
            
            let noti = CBMutableCharacteristic.init(type: CBUUID.init(string: notifyCharacteristicUUID), properties: .notify, value: nil, permissions: .readable)
            //特征值,只读
            let chart_0 = CBMutableCharacteristic.init(type: CBUUID.init(string: readCharacteristicUUID), properties: .read, value: nil, permissions: .readable)
            //特征值,只写
            let chart_1 = CBMutableCharacteristic.init(type: CBUUID.init(string: writeCharacteristicUUID), properties: .write, value: nil, permissions: .writeable)
            //描述符
            let des_0 = CBMutableDescriptor.init(type: cbu, value: "name")
            let des_1 = CBMutableDescriptor.init(type: cbu, value: "name")
            
            chart_0.descriptors = [des_0]
            chart_1.descriptors = [des_1]
            service1.characteristics = [chart_0,chart_1,noti]
            //增加1个服务
            phermgr.add(service1)
            tips.isHidden = false
            
        case CBManagerState.unauthorized: //无BLE权限
            NSLog("无BLE权限")
        case CBManagerState.poweredOff: //蓝牙未打开
            NSLog("蓝牙未开启")
        default:
            NSLog("状态无变化")
        }
          
    }

当服务加入以后,就可以开始广播数据了:

 func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
        print("peripheralManager didAdd")
        //加入服务以后再打开广播
        peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey:[CBUUID.init(string: serviceUUID1)],CBAdvertisementDataLocalNameKey:LocalNameKey])
    }

只有打开了服务,Central才能搜索到设备

当它作为Peripheral的时候,我们可以理解为服务器开启了,所以我们要为它添加应答的内容,如下:

func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
        print("peripheralManagerIsReady")
    }
    func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
        print("peripheralManagerDidStartAdvertising")
         
    }
    
    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
        print("subscribe")
        //订阅成功之后我们给它发送一个数据
//        self.sendData(characteristic: characteristic)
        didSendChara = characteristic
        if (timeAction != nil) {
            timeAction.invalidate()
            timeAction = nil
        }
        timeAction = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(sendData), userInfo: nil, repeats: true)
//        timeAction.fire()
    }
    //收到读的请求
    func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
        
        print("didReceiveRead")
        //最好做一下是否有读权限
        if request.characteristic.properties == CBCharacteristicProperties.read {
            
            request.value = Data.init(bytes: [0x02,0x03], count: 2)
            peripheral.respond(to: request, withResult: .success)
//            self.sendData(characteristic: request.characteristic)
        }else{
            peripheral.respond(to: request, withResult: .writeNotPermitted)
        }
        
    }
    func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
        print("didReceiveWrite")
        let request = requests[0]
        for req in requests {
            print(req.characteristic.uuid.uuidString)
        }
       //先检查是否有写权限
        if request.characteristic.properties == .write {
            peripheral.respond(to: request, withResult: .success)
            
        }else{
            peripheral.respond(to: request, withResult: .writeNotPermitted)
        }
    }
    
    //取消订阅
    func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
        
    }
​
//数据发送
    @objc func sendData(){
         let date = Date.init()
         let dateformate = DateFormatter.init()
         dateformate.dateFormat = "yyyy-MM-dd HH:mm:ss"
         let str = dateformate.string(from: date)
         let data = str.data(using:String.Encoding.utf8)!
         phermgr.updateValue(data, for: didSendChara as! CBMutableCharacteristic, onSubscribedCentrals: nil)
        print("sendData")
     }

再深入一步

为了再深入的看到整个CoreBluetooth的架构,我去整理了CoreBluetooth的结构脑图,方便后续继续开展学习和方便记忆。其实总的来看,所有的API都是在套接,一层套一层,最终到达上层的时候,我们只能看到少部分的内容了,我们在写封装库的时候可以参考着来写,那样整体库的逻辑就非常的清晰了。下面是脑图:


CoreBlueTooth.png

Demo地址:https://github.com/110201041018/CoreBluetoothDemo

你可能感兴趣的:(iOS 蓝牙开发 CoreBluetooth 应用)