iOS藍(lán)牙開(kāi)發(fā)

藍(lán)牙屬于近場(chǎng)通訊中的一種宛畦,iOS 中使用Core Bluetooth 框架實(shí)現(xiàn)藍(lán)牙通信, Core Bluetooth支持藍(lán)牙低功耗的4.0模式,就是通常說(shuō)稱(chēng)之的BLE渗常,在生活中BLE無(wú)處不在掠械,例如智能家居由缆,健身器材和智能玩具等,利用蘋(píng)果提供的Core Bluetooth框架可以實(shí)現(xiàn)和BLE設(shè)備進(jìn)行通信猾蒂。

藍(lán)牙中各角色的理解

在藍(lán)牙開(kāi)發(fā)中我們把提供服務(wù)的一方稱(chēng)之為周邊設(shè)備均唉,接收服務(wù)的一方稱(chēng)之為中央設(shè)備,典型的例子就是蘋(píng)果手表和iPhone配對(duì)時(shí)的關(guān)系肚菠,蘋(píng)果手表向iPhone提供用戶(hù)的運(yùn)動(dòng)數(shù)據(jù)舔箭,所以此種情況蘋(píng)果手表是周邊設(shè)備,iPhone是中央設(shè)備,在Core Bluetooth 框架中分別對(duì)應(yīng)如下:

  • centralManager:中央設(shè)備的處理類(lèi)
  • peripheralManager:周邊設(shè)備的處理類(lèi)

明確了周邊設(shè)備和中央設(shè)備后层扶,接下來(lái)是如何發(fā)現(xiàn)對(duì)方并建立連接箫章,在我們平時(shí)使用的手機(jī)搜索藍(lán)牙的過(guò)程中,都是先從搜索列表中選擇某個(gè)藍(lán)牙設(shè)備镜会,在進(jìn)行配對(duì)連接檬寂。peripheral通過(guò)廣播的形式向外界提供serviceservice會(huì)綁定一個(gè)獨(dú)一無(wú)二的UUID戳表,有BTSIG UUIDCustom UUID二種桶至,UUID用來(lái)確定中央設(shè)備連接周邊設(shè)備時(shí)確定身份用的。

每個(gè)service會(huì)有多個(gè)characteristic匾旭,characteristic也有自己的UUID镣屹,characteristic可以理解為周邊設(shè)備提供的具體服務(wù),其UUID用來(lái)區(qū)分提供的每一個(gè)具體服務(wù)价涝,因?yàn)橐粋€(gè)service是可以提供多種具體服務(wù)的女蜈,中央設(shè)備通過(guò)UUID來(lái)讀寫(xiě)這些服務(wù)。

在雙方建立了連接后就要商議如何發(fā)送和接受數(shù)據(jù)了飒泻,數(shù)據(jù)傳輸協(xié)議部分我們不用細(xì)究鞭光,Core Bluetooth都為我們處理好了,至于MTU最大最大傳輸單元現(xiàn)在是是271bytes泞遗,數(shù)據(jù)超過(guò)了就會(huì)分段發(fā)送惰许。

實(shí)戰(zhàn)演示

CBPeripheralManager

新建一個(gè)PeripheralViewController類(lèi)并繼承UIViewController,定義成員變量peripheralManager并初始化史辙,同時(shí)設(shè)置代理汹买,由于篇幅有限這里只貼出關(guān)鍵代碼:

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

Peripheral Manager delegate

代理必須實(shí)現(xiàn)的方法如下:

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
    }
  }

當(dāng)藍(lán)牙服務(wù)可用時(shí),需要?jiǎng)?chuàng)建service并關(guān)聯(lián)相應(yīng)的characteristic聊倔,代碼中的UUID都是定義的字符串常量晦毙。

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

通過(guò)startAdvertising方法來(lái)向外界發(fā)送廣播。

由于iOS的限制耙蔑,當(dāng)iOS設(shè)備作為周邊設(shè)備向外廣播時(shí)是無(wú)法利用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
      }
    }
  }

此回調(diào)會(huì)在中央設(shè)備訂閱了當(dāng)初廣播的characteristic時(shí)調(diào)用,這里我們準(zhǔn)備發(fā)送數(shù)據(jù)甸陌,發(fā)送數(shù)據(jù)的過(guò)程中和中央設(shè)備需要約定一個(gè)標(biāo)識(shí)表明數(shù)據(jù)是否發(fā)送完畢须揣,這里采用了EOM標(biāo)志作為結(jié)束位,采用二進(jìn)制流的形式進(jìn)行發(fā)送钱豁。

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

此回調(diào)在CBPeripheralManager準(zhǔn)備發(fā)送下一段數(shù)據(jù)時(shí)發(fā)送耻卡,這里一般用來(lái)實(shí)現(xiàn)保證分段數(shù)據(jù)按順序發(fā)送給中央設(shè)備。

  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()
    }
  }

此回調(diào)在中央設(shè)備針對(duì)響應(yīng)的characteristic發(fā)送數(shù)據(jù)給外圍設(shè)備時(shí)調(diào)用牲尺,這里我們模擬中央設(shè)備發(fā)送打開(kāi)地圖的指令給iPhone卵酪。

CBCentralManager

新建一個(gè)CentralViewController類(lèi)并繼承UIViewController,定義成員變量centralManager并初始化,同時(shí)設(shè)置代理溃卡,由于篇幅有限這里只貼出關(guān)鍵代碼:

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

Central Manager delegate

代理必須要實(shí)現(xiàn)的方法如下:

  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!)
  }

藍(lán)牙可用時(shí)開(kāi)始掃描溢豆,通過(guò)UUID掃描外圍設(shè)備廣播的服務(wù)。

  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強(qiáng)度塑煎,只有藍(lán)牙信號(hào)強(qiáng)度在一定范圍內(nèi)才開(kāi)始嘗試進(jìn)行連接沫换。

 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()
  }

以上是關(guān)于連接的幾個(gè)回調(diào)函數(shù),連接成功后就停止掃描最铁,然后調(diào)用peripheral.discoverServices方法讯赏,這會(huì)來(lái)到Peripheral Delegate中的相應(yīng)代理方法。

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
      }
    }
  }

此回調(diào)用來(lái)發(fā)現(xiàn)services冷尉,實(shí)際開(kāi)發(fā)中這里可能用列表展示發(fā)現(xiàn)的服務(wù)漱挎,讓用戶(hù)進(jìn)行相應(yīng)的選擇。

  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)
      }
    }
  }

此回調(diào)對(duì)應(yīng)peripheralManager.updateValue這個(gè)方法雀哨,能拿到外圍設(shè)備發(fā)送過(guò)來(lái)的數(shù)據(jù)磕谅。

 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...")
    }
  }

此回調(diào)處理外圍設(shè)備的characteristic通知,比如下線(xiàn)或者離開(kāi)的情況雾棺,這里進(jìn)行簡(jiǎn)單的打印膊夹。

總結(jié)

對(duì)藍(lán)牙開(kāi)發(fā)中的外圍設(shè)備,中央設(shè)備捌浩,UUID放刨,servicecharacteristic等基本概念進(jìn)行了簡(jiǎn)單介紹,并利用Core Bluetooth 框架進(jìn)行了簡(jiǎn)單的demo演示尸饺,主要是需要理解幾個(gè)特定代理方法即可进统,同時(shí)由于iOS的限制,iPhone在作為外設(shè)時(shí)在廣播的時(shí)候是不能發(fā)送額外數(shù)據(jù)的浪听,這點(diǎn)需要注意螟碎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市迹栓,隨后出現(xiàn)的幾起案子掉分,更是在濱河造成了極大的恐慌,老刑警劉巖克伊,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叉抡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡答毫,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)季春,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)洗搂,“玉大人,你說(shuō)我怎么就攤上這事≡拍矗” “怎么了撵颊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)惫叛。 經(jīng)常有香客問(wèn)我倡勇,道長(zhǎng),這世上最難降的妖魔是什么嘉涌? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任妻熊,我火速辦了婚禮,結(jié)果婚禮上仑最,老公的妹妹穿的比我還像新娘扔役。我一直安慰自己,他們只是感情好警医,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布亿胸。 她就那樣靜靜地躺著,像睡著了一般预皇。 火紅的嫁衣襯著肌膚如雪侈玄。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天吟温,我揣著相機(jī)與錄音序仙,去河邊找鬼。 笑死溯街,一個(gè)胖子當(dāng)著我的面吹牛诱桂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播呈昔,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼挥等,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了堤尾?” 一聲冷哼從身側(cè)響起肝劲,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎郭宝,沒(méi)想到半個(gè)月后辞槐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粘室,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年榄檬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衔统。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹿榜,死狀恐怖海雪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舱殿,我是刑警寧澤奥裸,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站沪袭,受9級(jí)特大地震影響湾宙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冈绊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一侠鳄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧焚碌,春花似錦畦攘、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鹃骂,卻和暖如春台盯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背畏线。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工静盅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寝殴。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓蒿叠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蚣常。 傳聞我的和親對(duì)象是個(gè)殘疾皇子市咽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容