要和藍芽裝置進行連線,需要幾個物件來實作
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 去找是否是和裝置通訊的介面囉
targetVC.char = btServices[sender!["section"] as! Int].characteristics[sender!["row"] as! Int]範例中這部分是否可幫忙解決一下,轉swift3.0時發現。error:Type "Any"has no subscript member~感謝
回覆刪除在 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"]!]
}
相當感謝~因為剛接觸BLE,你的範例相當完善。在write 傳送 data部分是否可以完善下。另外我看了一些Nordic 原始碼APP,他在傳送字串部分加上編碼 let data = aText.data(using: String.Encoding.utf8)
回覆刪除看你的文章似乎也有提到轉碼,如果只要傳送字串的話,可行嗎?是否能補充在你的範例上。~~謝謝您的分享!!!
write 的部分我在第三篇有寫吧,然後編碼的部分是要看收訊息的地方要使用哪種編碼,傳訊息的就需要把字串用那該編碼傳送啊,反正最後送出的都是 Data
刪除了解~感謝~
刪除抱歉~我如果寫了以下的方式對嗎 ?執行下沒問題。我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
}
傳送的時候都是傳送 Data,跟是不是 string 無關,要傳送 string 就是把 string 轉成 Data 在傳就好,現在重點是你的 $left# 是 Encoding 成 utf8 的編碼傳送的,如果你的接收端是以 ASCII 去判定,那當然判定不出來,同樣的字串內容,不同的編碼方式,轉出來的 Data 當然不同囉,不過你的狀況 $left# 用 utf8 和 ascii 編碼是相同的,所以建議去藍芽裝置上 Log 看到底收到什麼在做下一步
刪除了解~感謝您的說明
刪除請問 bbConnect這個是什麼物件,
回覆刪除bbConnect.title = "Connected"
bbConnect.enabled = false
不懂耶
bbConnect 是 BarButtonItem 的物件
刪除這邊只是想再顯示上當已經連線後就 disable 這個按鈕來知道成功連線並顯示 Connected
在上面 "此藍芽裝置客製化 service" 下方的圖右上角那個按鈕
沒什麼特別用途
作者已經移除這則留言。
回覆刪除您好~想了解BLE連線這邊,假設我現在掃到三個設備,其中一個是我的目標,我想把我目標的BLEservice uuid儲存到coredata裡面,下次使用時,我就不掃描,可能直接設一個button點一下就直接連線到我的目標設備,這邊我的問題是這個button裡面我要怎設計??感謝回覆
回覆刪除這個問題太廣了,不知道要怎麼回答耶,你可以在背景掃描,然後先抓到 device 和 service,然後 button 按下去後就連線,或者是按下去後在掃描加連線,做法很多,想怎麼設計就怎麼設計啊
刪除