iOS蓝牙开发

蓝牙属于近场通讯中的一种,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在作为外设时在广播的时候是不能发送额外数据的,这点需要注意。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • iOS的蓝牙数据接收以及发送 名词:Central(中心设备)、Peripheral(外围设备)、advertis...
    TianBai阅读 22,596评论 54 304
  • 首先先明白几个名词:Central(中心设备)、Peripheral(外围设备)、advertising(广告)、...
    shuaikun阅读 414评论 0 0
  • iOS 中使用 Core Bluetooth 框架实现蓝牙通信。Core Bluetooth 是基于蓝牙 4.0 ...
    欧大帅Allen阅读 2,706评论 0 3
  • 相比于网络请求,蓝牙是一种低功耗(low-energy)的通信手段。目前iOS的开发都是针对CoreBluetoo...
    huxinwen阅读 10,444评论 0 11
  • @[TOC](iOS 蓝牙开发 ) 1.蓝牙简介 蓝牙模式简介蓝牙开发分为两种模式,中心模式(central),和...
    孔雨露阅读 2,288评论 1 2