美文网首页
iOS蓝牙开发

iOS蓝牙开发

作者: MambaYong | 来源:发表于2022-08-07 14:38 被阅读0次

    蓝牙属于近场通讯中的一种,iOS 中使用Core Bluetooth 框架实现蓝牙通信, Core Bluetooth支持蓝牙低功耗的4.0模式,就是通常说称之的BLE,在生活中BLE无处不在,例如智能家居,健身器材和智能玩具等,利用苹果提供的Core Bluetooth框架可以实现和BLE设备进行通信。

    蓝牙中各角色的理解

    在蓝牙开发中我们把提供服务的一方称之为周边设备,接收服务的一方称之为中央设备,典型的例子就是苹果手表和iPhone配对时的关系,苹果手表向iPhone提供用户的运动数据,所以此种情况苹果手表是周边设备,iPhone是中央设备,在Core Bluetooth 框架中分别对应如下:

    • centralManager:中央设备的处理类
    • peripheralManager:周边设备的处理类

    明确了周边设备和中央设备后,接下来是如何发现对方并建立连接,在我们平时使用的手机搜索蓝牙的过程中,都是先从搜索列表中选择某个蓝牙设备,在进行配对连接。peripheral通过广播的形式向外界提供serviceservice会绑定一个独一无二的UUID,有BTSIG UUIDCustom UUID二种,UUID用来确定中央设备连接周边设备时确定身份用的。

    每个service会有多个characteristiccharacteristic也有自己的UUIDcharacteristic可以理解为周边设备提供的具体服务,其UUID用来区分提供的每一个具体服务,因为一个service是可以提供多种具体服务的,中央设备通过UUID来读写这些服务。

    在双方建立了连接后就要商议如何发送和接受数据了,数据传输协议部分我们不用细究,Core Bluetooth都为我们处理好了,至于MTU最大最大传输单元现在是是271bytes,数据超过了就会分段发送。

    实战演示

    CBPeripheralManager

    新建一个PeripheralViewController类并继承UIViewController,定义成员变量peripheralManager并初始化,同时设置代理,由于篇幅有限这里只贴出关键代码:

    peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
    

    Peripheral Manager delegate

    代理必须实现的方法如下:

    extension PeripheralViewController: CBPeripheralManagerDelegate {
      func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        switch peripheral.state {
        case .poweredOn:
          textCharacteristic = CBMutableCharacteristic(type: textCharacteristicUUID, properties: .notify, value: nil, permissions: .readable)
          mapCharacteristic = CBMutableCharacteristic(type: mapCharacteristicUUID, properties: .writeWithoutResponse, value: nil, permissions: .writeable)
          let service = CBMutableService(type: TextOrMapServiceUUID, primary: true)
          service.characteristics = [textCharacteristic, mapCharacteristic]
          peripheralManager.add(service)
        default: return
        }
      }
    

    当蓝牙服务可用时,需要创建service并关联相应的characteristic,代码中的UUID都是定义的字符串常量。

    peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [TextOrMapServiceUUID]])
    

    通过startAdvertising方法来向外界发送广播。

    由于iOS的限制,当iOS设备作为周边设备向外广播时是无法利用CBAdvertisementDataManufacturerDataKey携带manufacturer data的。

      func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral,
        didSubscribeTo characteristic: CBCharacteristic) {
        guard characteristic == textCharacteristic else { return }
        prepareDataAndSend()
      }
    
      func prepareDataAndSend() {
        guard let data = textView.text.data(using: .utf8) else { return }
        self.dataToSend = data
        sendDataIndex = 0
        sendData()
      }
    
    func sendData() {
        if sendingEOM {
          let didSend = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: textCharacteristic, onSubscribedCentrals: nil)
          if didSend {
            sendingEOM = false
            print("Sent: EOM")
          }
          return
        }
        let numberOfBytes = (dataToSend as NSData).length
        guard sendDataIndex < numberOfBytes else { return }
        var didSend = true
        while didSend {
          var amountToSend = numberOfBytes - sendDataIndex
          if amountToSend > notifyMTU {
            amountToSend = notifyMTU
          }
    
          let chunk = dataToSend.withUnsafeBytes{(body: UnsafePointer<UInt8>) in
            return Data(
              bytes: body + sendDataIndex,
              count: amountToSend
            )
          }
          didSend = peripheralManager.updateValue(chunk, for: textCharacteristic, onSubscribedCentrals: [])
          if !didSend { return }
    
          guard let stringFromData = String(data: chunk, encoding: .utf8) else { return }
          print("Sent: \(stringFromData)")
          sendDataIndex += amountToSend
          if sendDataIndex >= dataToSend.count {
            sendingEOM = true
            let eomSent = peripheralManager.updateValue("EOM".data(using: .utf8)!, for: textCharacteristic, onSubscribedCentrals: nil)
            if eomSent {
              sendingEOM = false
              print("Sent: EOM")
            }
            return
          }
        }
      }
    
    

    此回调会在中央设备订阅了当初广播的characteristic时调用,这里我们准备发送数据,发送数据的过程中和中央设备需要约定一个标识表明数据是否发送完毕,这里采用了EOM标志作为结束位,采用二进制流的形式进行发送。

      func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
        sendData()
      }
    

    此回调在CBPeripheralManager准备发送下一段数据时发送,这里一般用来实现保证分段数据按顺序发送给中央设备。

      func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
        guard let request = requests.first, request.characteristic == mapCharacteristic else {
          peripheral.respond(to: requests.first!, withResult: .attributeNotFound)
          return
        }
        map() { locationManager?.stopUpdatingLocation() }
        peripheral.respond(to: request, withResult: .success)
      }
    
      fileprivate func map(completionHandler: () -> Void) {
        locationManager = CLLocationManager()
        locationManager?.delegate = self
        locationManager?.desiredAccuracy = kCLLocationAccuracyBest
        locationManager?.requestWhenInUseAuthorization()
        if CLLocationManager.authorizationStatus() == .authorizedWhenInUse || CLLocationManager.authorizationStatus() == .authorizedAlways {
          locationManager?.startUpdatingLocation()
        }
      }
    

    此回调在中央设备针对响应的characteristic发送数据给外围设备时调用,这里我们模拟中央设备发送打开地图的指令给iPhone

    CBCentralManager

    新建一个CentralViewController类并继承UIViewController,定义成员变量centralManager并初始化,同时设置代理,由于篇幅有限这里只贴出关键代码:

    centralManager = CBCentralManager(delegate: self, queue: nil)
    

    Central Manager delegate

    代理必须要实现的方法如下:

      func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn: scan()
        case .poweredOff, .resetting: cleanup()
        default: return
        }
      }
    
      func scan() {
        centralManager.scanForPeripherals(withServices: [TextOrMapServiceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: NSNumber(value: true as Bool)])
      }
    
      func cleanup() {
        guard discoveredPeripheral?.state != .disconnected,
          let services = discoveredPeripheral?.services else {
            centralManager.cancelPeripheralConnection(discoveredPeripheral!)
            return
        }
        for service in services {
          if let characteristics = service.characteristics {
            for characteristic in characteristics {
              if characteristic.uuid.isEqual(textCharacteristicUUID) {
                if characteristic.isNotifying {
                  discoveredPeripheral?.setNotifyValue(false, for: characteristic)
                  return
                }
              }
            }
          }
        }
        centralManager.cancelPeripheralConnection(discoveredPeripheral!)
      }
    

    蓝牙可用时开始扫描,通过UUID扫描外围设备广播的服务。

      func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        guard RSSI_range.contains(RSSI.intValue) && discoveredPeripheral != peripheral else { return }
        discoveredPeripheral = peripheral
        centralManager.connect(peripheral, options: [:])
      }
    

    需要检查RSSI强度,只有蓝牙信号强度在一定范围内才开始尝试进行连接。

     func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        if let error = error { print(error.localizedDescription) }
        cleanup()
      }
    
      func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        centralManager.stopScan()
        data.removeAll()
        peripheral.delegate = self
        peripheral.discoverServices([TextOrMapServiceUUID])
      }
    
      func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        if (peripheral == discoveredPeripheral) {
          cleanup()
        }
        scan()
      }
    

    以上是关于连接的几个回调函数,连接成功后就停止扫描,然后调用peripheral.discoverServices方法,这会来到Peripheral Delegate中的相应代理方法。

    Peripheral Delegate

      func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let error = error {
          print(error.localizedDescription)
          cleanup()
          return
        }
    
        guard let services = peripheral.services else { return }
        for service in services {
          peripheral.discoverCharacteristics([textCharacteristicUUID, mapCharacteristicUUID], for: service)
        }
      }
    
      func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let error = error {
          print(error.localizedDescription)
          cleanup()
          return
        }
    
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
          if characteristic.uuid == textCharacteristicUUID {
            textCharacteristic = characteristic
            peripheral.setNotifyValue(true, for: characteristic)
          } else if characteristic.uuid == mapCharacteristicUUID {
            mapCharacteristic = characteristic
          }
        }
      }
    

    此回调用来发现services,实际开发中这里可能用列表展示发现的服务,让用户进行相应的选择。

      func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
          print(error.localizedDescription)
          return
        }
    
        if characteristic == textCharacteristic {
          guard let newData = characteristic.value else { return }
          let stringFromData = String(data: newData, encoding: .utf8)
    
          if stringFromData == "EOM" {
            textView.text = String(data: data, encoding: .utf8)
            data.removeAll()
          } else {
            data.append(newData)
          }
        }
      }
    

    此回调对应peripheralManager.updateValue这个方法,能拿到外围设备发送过来的数据。

     func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error { print(error.localizedDescription) }
        guard characteristic.uuid == textCharacteristicUUID else { return }
        if characteristic.isNotifying {
          print("Notification began on \(characteristic)")
        } else {
          print("Notification stopped on \(characteristic). Disconnecting...")
        }
      }
    

    此回调处理外围设备的characteristic通知,比如下线或者离开的情况,这里进行简单的打印。

    总结

    对蓝牙开发中的外围设备,中央设备,UUIDservicecharacteristic等基本概念进行了简单介绍,并利用Core Bluetooth 框架进行了简单的demo演示,主要是需要理解几个特定代理方法即可,同时由于iOS的限制,iPhone在作为外设时在广播的时候是不能发送额外数据的,这点需要注意。

    相关文章

      网友评论

          本文标题:iOS蓝牙开发

          本文链接:https://www.haomeiwen.com/subject/ippwwrtx.html