fb-script

2015年10月19日 星期一

iOS CoreBluetooth swift 2 連線客製化藍芽 BLE 模組 - ( 2 ) 連線

要和藍芽裝置進行連線,需要幾個物件來實作
1. 掃描到藍芽裝置時的 centralManager
2. 藍芽裝置的 peripheral

所以先加入一個新的 ViewController 在點擊 tableView 的時候去切換 segue,並把 centralManger 和要連線的藍芽 peripheral 送過去

這邊建立一個 BTDeviceConfigViewController 的 ViewController 和對應的 storyboard 視圖

class BTDeviceConfigViewController: UIViewController,UITableViewDataSource, UITableViewDelegate, CBPeripheralDelegate, CBCentralManagerDelegate {
    var peripheral: CBPeripheral!
    var centralManager: CBCentralManager!
}

所以當切換 segue 的時候要把需要的 peripheral 和 centralManager 帶過去喔! 這邊順便帶了偵測到的裝置名稱

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)

    self.performSegueWithIdentifier("sgToBTDeviceConfig", sender: BTPeripheral[indexPath.row])
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "sgToBTDeviceConfig" {
       let targetVC = segue.destinationViewController as! BTDeviceConfigViewController
       targetVC.peripheral = sender as! CBPeripheral
       targetVC.btDeviceName = (sender as! CBPeripheral).name
       targetVC.centralManager = self.myCenteralManager
    }
}

好der~ 所以在這個新的 BTDeviceConfigViewController 中初始化的時候我們就有三個東西,一個是 centralManager、一個 peripheral、一個 name

接下來的步驟都是在 BTDeviceConfigViewController 之中囉

1.連線
這個步驟滿簡單,首先要將傳送過來的 centralManager 和 peripheral 的 delegate 都設為當前這個 UIViewController,也就是 BTDeviceConfigViewController,這樣才可能監聽到發生的事件,也就是為什麼最上面在製作 BTDeviceConfigViewController 的時後會有那些 Delegate 的實作啦!
override func viewDidLoad() {
    super.viewDidLoad()
    navTitle.title = btDeviceName
    peripheral.delegate = self
    centralManager.delegate = self
    centralManager.connectPeripheral(peripheral, options: nil)

    tbvServices.delegate = self
    tbvServices.dataSource = self
    btServices = []
    if peripheral.state == CBPeripheralState.Connected {
        bbConnect.title = "Connected"
        bbConnect.enabled = false
    }

    tbvServices.registerNib(UINib(nibName: "BTServiceTableViewCell", bundle: nil), forCellReuseIdentifier: "BTServiceTableViewCell")
}

這邊可以看到一個新的 tableViewCell 物件 BTServiceTableViewCell,等等會解釋這是做什麼用的

navTitle 是 navigation bar 的 title,這邊讓他就是這個藍芽裝置的名稱
bvServices 是放 service 列表的 tableview
btServices 是放搜尋到的服務的容器

這邊關係說明一下,一個 peripheral 可能會有多個 service,一個 service 可能會有多個 characteristic,而真正拿來和裝置溝通的是各種不同的 characteristic,此系列的目的就是要找到客製化的藍芽裝置到底是要哪幾個 characteristic 做通訊,如果是要用正規藍芽的 characteristic 定義的欄位,可以參考藍芽官方網站就能知道哪幾個 characteristic 是代表什麼意思

連線就是那行精美的centralManager.connectPeripheral(peripheral, options: nil),後面的 option 是可以放篩選條件的,目前我放 nil 就所有東西都會篩選到,那到底篩選什麼東西呢?
當執行connectPeripheral,我們就會拿到這個 peripheral 所提供的 service (CBService) 和所屬該 service 的特徵值(CBCharacteristic)。因為 delegate 已經設定給當前 UIViewController (也就是 BTDeviceConfigViewController) 所以在 func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral)這個 callback 中就可以得知執行connectPeripheral 方法是否成功,當成功後就要去對 peripheral 取得 service

func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) {
    if peripheral.state == CBPeripheralState.Connected {
        bbConnect.title = "Connected"
        bbConnect.enabled = false
        peripheral.discoverServices(nil)
    } 
}

沒錯就是那discoverServices 的方法啦,這個方法會去掃描 peripheral 所提供的服務(nil 不設條件就會拿到所有服務),然後會在func peripheral(peripheral:CBPeripheral, didDiscoverServices, error: NSError?)這個 callback 中拿到 service

為了方便起見這邊定義個類別來存放資訊

class BTServiceInfo {
    var service: CBService!
    var characteristics: [CBCharacteristic]
    init(service: CBService, characteristics: [CBCharacteristic]) {
        self.service = service
        self.characteristics = characteristics
    }
}

因為一個 service 可能會有很多個 characteristic,所以就把一起的綁在同一個物件裡(偷懶)
顧名思義就是放 Service 資訊和 Characteristic 資訊。好那就把獲得到的 Service 資訊塞到容器裡吧

func peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError?) {

    for serviceObj in peripheral.services! {
        let service:CBService = serviceObj
        let isServiceIncluded = self.btServices.filter({ (item: BTServiceInfo) -> Bool in
            return item.service.UUID == service.UUID
        }).count
        if isServiceIncluded == 0 {
            btServices.append(BTServiceInfo(service: service, characteristics: []))
        }
        peripheral.discoverCharacteristics(nil, forService: service)

    }
}

btServices 裡面放的就是 BTServiceInfo 物件,所以在掃描到新的 service 的時候先判斷是否已經在容器裡,如果沒有就加入。最後對每個 service 使用discoverCharacteristics 去獲得該 service 所有的 characteristic。沒錯就是peripheral.discoverCharacteristics(nil, forService: service)這行,nil 一樣是不設限的意思了。執行這行後會在func peripheral(peripheral:CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?)此 callback 中獲得該 service 所掃描到的 characteristic,這邊就把它加到所屬該 service 的物件中

func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) {
    let serviceCharacteristics = service.characteristics
    for item in btServices {
        if item.service.UUID == service.UUID {
            item.characteristics = serviceCharacteristics!
            break
        }
    }
    tbvServices.reloadData()
}
自訂的 TableviewCell
DESC: 是放 description
Prop: 是放 Property
Value: 就是放該 characteristic 的值
Noti: 是顯示該 characteristic 是否支援 Notification
UUID: 該 characteristic 的 UUID

這邊 reload table 後就可以看到這樣的畫面。我這邊的藍芽裝置有掃描到兩個 service,一個是 Device Info,這個是官方規格一定有的。另一個就是這台藍芽裝置客製化的部分

此藍芽裝置客製化 service

可以看到各個 service 所擁有的 Characteristic 列表和該 Characteristic 的一些屬性

再來就是要找哪個( 或哪幾個 ) Characteristic 是和裝置通訊的介面了,這邊最後列出 cellForRowAtIndexPath 給大家參考

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("BTServiceTableViewCell") as! BTServiceTableViewCell
    cell.lbUUID.text = btServices[indexPath.section].characteristics[indexPath.row].UUID.UUIDString
    cell.lbDesc.text = btServices[indexPath.section].characteristics[indexPath.row].UUID.description

    cell.lbProperties.text = String(format: "0x%02X", btServices[indexPath.section].characteristics[indexPath.row].properties.rawValue)
    cell.lbValue.text = btServices[indexPath.section].characteristics[indexPath.row].value?.description ?? "null"
    cell.lbNotifying.text = btServices[indexPath.section].characteristics[indexPath.row].isNotifying.description
    cell.lbPropertyContent.text = btServices[indexPath.section].characteristics[indexPath.row].getPropertyContent()
    return cell
}

這邊對每個 Characteristic 的 property 要如何取得呢?其實每個 Characteristic 的 property 是以二進位去做區隔,例如可以 Read 就是在 Read 那個位元的位置是 1,假設 Read 在第一位元,Write 在第二位元,那 Read only 就可以表示成 10、Write only 就表示成 01、Read / Write 就可以表示成 11。詳細定義要參考官方設定囉,這邊只是舉例假設

在 iOS 中的 Characteristic 可以直接用 intersect 的方法去判斷是否含有某個屬性,這邊就將 CBCharacteristic 做一個 extension 就好了

extension CBCharacteristic {

    func isWritable() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.Write)) != []
    }

    func isReadable() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.Read)) != []
    }

    func isWritableWithoutResponse() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.WriteWithoutResponse)) != []
    }

    func isNotifable() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.Notify)) != []
    }

    func isIdicatable() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.Indicate)) != []
    }

    func isBroadcastable() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.Broadcast)) != []
    }

    func isExtendedProperties() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.ExtendedProperties)) != []
    }

    func isAuthenticatedSignedWrites() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.AuthenticatedSignedWrites)) != []
    }

    func isNotifyEncryptionRequired() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.NotifyEncryptionRequired)) != []
    }

    func isIndicateEncryptionRequired() -> Bool {
        return (self.properties.intersect(CBCharacteristicProperties.IndicateEncryptionRequired)) != []
    }


    func getPropertyContent() -> String {
        var propContent = ""
        if (self.properties.intersect(CBCharacteristicProperties.Broadcast)) != [] {
            propContent += "Broadcast,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.Read)) != [] {
            propContent += "Read,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.WriteWithoutResponse)) != [] {
            propContent += "WriteWithoutResponse,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.Write)) != [] {
            propContent += "Write,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.Notify)) != [] {
            propContent += "Notify,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.Indicate)) != [] {
            propContent += "Indicate,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.AuthenticatedSignedWrites)) != [] {
            propContent += "AuthenticatedSignedWrites,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.ExtendedProperties)) != [] {
            propContent += "ExtendedProperties,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.NotifyEncryptionRequired)) != [] {
            propContent += "NotifyEncryptionRequired,"
        }
        if (self.properties.intersect(CBCharacteristicProperties.IndicateEncryptionRequired)) != [] {
            propContent += "IndicateEncryptionRequired,"
        }
        return propContent
    }
}

這就是在 tableviewCellForRowAtIndex 中所用的方法

下一步就是去針對每個 Characteristic 去找是否是和裝置通訊的介面囉

上一篇 連線客製化藍芽 BLE 模組 - ( 1 ) 掃描

下一篇 連線客製化藍芽 BLE 模組 - ( 3 ) 通訊

13 則留言:

  1. targetVC.char = btServices[sender!["section"] as! Int].characteristics[sender!["row"] as! Int]範例中這部分是否可幫忙解決一下,轉swift3.0時發現。error:Type "Any"has no subscript member~感謝

    回覆刪除
    回覆
    1. 在 swift 3, prepare for segue 中的 sender 從 AnyObject? 變成 Any?, 所以會跟你說 sender 沒有 subscript, 這邊只要將 sender 轉型成 dictionary 就可以了

      if let sender = sender as? [String: Int] {
      targetVC.char=btServices[sender["section"]!].characteristics[sender["row"]!]
      }

      刪除
  2. 相當感謝~因為剛接觸BLE,你的範例相當完善。在write 傳送 data部分是否可以完善下。另外我看了一些Nordic 原始碼APP,他在傳送字串部分加上編碼 let data = aText.data(using: String.Encoding.utf8)
    看你的文章似乎也有提到轉碼,如果只要傳送字串的話,可行嗎?是否能補充在你的範例上。~~謝謝您的分享!!!

    回覆刪除
    回覆
    1. write 的部分我在第三篇有寫吧,然後編碼的部分是要看收訊息的地方要使用哪種編碼,傳訊息的就需要把字串用那該編碼傳送啊,反正最後送出的都是 Data

      刪除
    2. 抱歉~我如果寫了以下的方式對嗎 ?執行下沒問題。我print出dataToSend顯示6 bytes。我接收端是設定接收到$left#燈號會亮,目前是沒反應,我猜是不是他不是傳字串,因為後面編碼過。但我如果把傳遞資料改為String。writeValue後面會顯示錯誤,必須為Data。是不是在swift 3.0不能傳遞string
      @IBAction func btnwrite(_ sender: Any) {
      writeValue()
      }
      func writeValue() {

      let dataToSend: Data = "$left#".data(using: String.Encoding.utf8)!

      print(dataToSend)

      peripheral.writeValue(dataToSend, for: char, type: CBCharacteristicWriteType.withoutResponse) //Writing the data to the peripheral
      }

      刪除
    3. 傳送的時候都是傳送 Data,跟是不是 string 無關,要傳送 string 就是把 string 轉成 Data 在傳就好,現在重點是你的 $left# 是 Encoding 成 utf8 的編碼傳送的,如果你的接收端是以 ASCII 去判定,那當然判定不出來,同樣的字串內容,不同的編碼方式,轉出來的 Data 當然不同囉,不過你的狀況 $left# 用 utf8 和 ascii 編碼是相同的,所以建議去藍芽裝置上 Log 看到底收到什麼在做下一步

      刪除
  3. 請問 bbConnect這個是什麼物件,
    bbConnect.title = "Connected"
    bbConnect.enabled = false
    不懂耶

    回覆刪除
    回覆
    1. bbConnect 是 BarButtonItem 的物件
      這邊只是想再顯示上當已經連線後就 disable 這個按鈕來知道成功連線並顯示 Connected

      在上面 "此藍芽裝置客製化 service" 下方的圖右上角那個按鈕

      沒什麼特別用途

      刪除
  4. 作者已經移除這則留言。

    回覆刪除
  5. 您好~想了解BLE連線這邊,假設我現在掃到三個設備,其中一個是我的目標,我想把我目標的BLEservice uuid儲存到coredata裡面,下次使用時,我就不掃描,可能直接設一個button點一下就直接連線到我的目標設備,這邊我的問題是這個button裡面我要怎設計??感謝回覆

    回覆刪除
    回覆
    1. 這個問題太廣了,不知道要怎麼回答耶,你可以在背景掃描,然後先抓到 device 和 service,然後 button 按下去後就連線,或者是按下去後在掃描加連線,做法很多,想怎麼設計就怎麼設計啊

      刪除