iOS App 通过CoreBluetooth(Swift 蓝牙)和Android(低功耗蓝牙BLE)交互。

> 概念

如果你是进来找代码的,那么直接拉到最后!!!
本文概念参考的了Pein_Ju的文章BLE蓝牙开发—Swift版 本文更像是是偏向于在工作中记录和实践性,大佬请随意鄙视😅。我的代码连接放在最后。

  1. 现在iOS BLE开发一般调用的是CoreBluetooth系统原生库开发的蓝牙4.0以上的低功耗版本,其他连接方式和版本的暂不讨论。

  2. 蓝牙术语:

    * CBCentralManager //系统蓝牙设备管理对象
    * CBPeripheral //外围设备
    * CBService //外围设备的服务或者服务中包含的服务
    * CBCharacteristic //服务的特性
    * CBDescriptor //特性的描述符
    

    关系图如下:


    关系图
    1. 模式 & 步骤
      • 中心模式 Client

        1. 建立中心角色 CBCentralManager
        2. 扫描外设 cancelPeripheralConnection
        3. 发现外设 didDiscoverPeripheral
        4. 连接外设 connectPeripheral
        5. 扫描外设中的服务 discoverServices
        6. 发现并获取外设中的服务 didDiscoverServices
        7. 扫描外设对应服务的特征 discoverCharacteristics
        8. 发现并获取外设对应服务的特征 didDiscoverCharacteristicsForService
        9. 给对应特征写数据 writeValue:forCharacteristic:type:
        10. 订阅特征的通知 setNotifyValue:forCharacteristic:
        11. 根据特征读取数据 didUpdateValueForCharacteristic
      • 外设模式 Server --->

        1. 建立外设角色
        2. 设置本地外设的服务和特征
        3. 发布外设和特征
        4. 广播服务
        5. 响应中心的读写请求
        6. 发送更新的特征值,订阅中心
        • Android提供服务参考这里
        • iOS也可以作为外设(Server)参考这里

> Tips

  • 用上面的方式进行扫描后能获得的设备是正在广播的设备。这就可能和系统的列表不一样,连接的时候需要Android作为Server。
  • 需要注意外设,服务,特征之间的uuid,断线重连是用的Peripherals的uuid不要弄混了😂

> 具体连接步骤

方式1 原生连接
1.实现代理及代理方法 CBCentralManagerDelegate,CBPeripheralDelegate
2.在代理方法 centralManagerDidUpdateState 中检测到蓝牙设备的状态是poweredOn 才能开始扫描设备,要不然找不到~

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        
        tempInputView.text = "初始化对象后,来到centralManagerDidUpdateState"
        
        switch central.state {
        case .unknown:
            print("CBCentralManager state:", "unknown")
            break
        case .resetting:
            print("CBCentralManager state:", "resetting")
            break
        case .unsupported:
            print("CBCentralManager state:", "unsupported")
            break
        case .unauthorized:
            print("CBCentralManager state:", "unauthorized")
            break
        case .poweredOff:
            print("CBCentralManager state:", "poweredOff")
            break
        case .poweredOn:
            print("CBCentralManager state:", "poweredOn")
            //MARK: -3.扫描周围外设(支持蓝牙)
            // 第一个参数,传外设uuid,传nil,代表扫描所有外设
            self.addInputString(str: "开始扫描设备")
            central.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: NSNumber.init(value: false)])
        }
    }

3.发现设备回调的方法是:

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        
        print("=============================start")
        
        if (peripheral.name != nil && peripheral.name! != "xxb") { //排除 xxb
            
            print("peripheral.name = \(peripheral.name!)")
            print("central = \(central)")
            print("peripheral = \(peripheral)")
            print("RSSI = \(RSSI)")
            print("advertisementData = \(advertisementData)")
            
            deviceList.append(peripheral)
            
            tableView.reloadData()
        }
        print("=============================end")

    }

注意:这里面是每寻找到一个设备就会回调一次这个方法。

4.选中列表中其中一个点击进行连接:

self.addInputString(str: "链接设备")
            central.stopScan()
            central.cancelPeripheralConnection(p)
            central.connect(p, options: nil)

5.连接成功,连接失败的回调,其中连接成功了会记录对应的外设并且开始寻找服务

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        //设备链接成功
        self.addInputString(str: "链接成功=====>\(peripheral.name ?? "~~")")
        peripheralSelected = peripheral
        peripheralSelected!.delegate = self
        peripheralSelected!.discoverServices(nil) // 开始寻找Services。传入nil是寻找所有Services
    }
    
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        //设备链接失败
        self.addInputString(str: "链接失败=====>\(peripheral.name ?? "~~")")
        
    }

6.寻找到对应的服务特征会回调到peripheral didDiscoverServices 如果这里有和后台商量好的对一个的service可以做判断,当前demo是传入了nil,发现所有service,并且利用前面保存好的外设去调用发现特征,这里传入nil,和前文的意思相同。

    //请求周边去寻找他的服务特征
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        
        if error != nil {
            self.addInputString(str: "didDiscoverServices error ====> \(error.debugDescription) ")
            return
        }
    
        guard let serArr = peripheral.services else {
            self.addInputString(str: "Peripheral services is nil ")
            return
        }
        
      
        for ser in serArr {
            
            self.addInputString(str: "服务的UUID \(ser.uuid)")
            self.peripheralSelected!.discoverCharacteristics(nil, for: ser)
        }

        self.addInputString(str: "Peripheral 开始寻找特征 ")
        
    }

7.peripheral外设搜索服务后对应的特征回调信息方法是:peripheral didDiscoverCharacteristicsFor

 //找特征的回调
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        
        if error != nil { self.addInputString(str: "服务的回调error \(error.debugDescription)");return}
        
        guard let serviceCharacters = service.characteristics else {
            self.addInputString(str: "service.characteristics 为空")
            return
        }
        
        for characteristic in serviceCharacters {
            self.addInputString(str: "--------------------------characteristic")
            self.addInputString(str: "特征UUID \(characteristic.uuid)")
            self.addInputString(str: "uuidString \(characteristic.uuid.uuidString)")
            peripheralSelected!.setNotifyValue(true, for: characteristic) //接受通知
            //判断类型 <=========> 有问题的。
            /*
             CBCharacteristicPropertyBroadcast                                                = 0x01,
             CBCharacteristicPropertyRead                                                    = 0x02,
             CBCharacteristicPropertyWriteWithoutResponse                                    = 0x04,
             CBCharacteristicPropertyWrite                                                    = 0x08,
             CBCharacteristicPropertyNotify                                                    = 0x10,
             CBCharacteristicPropertyIndicate                                                = 0x20,
             CBCharacteristicPropertyAuthenticatedSignedWrites                                = 0x40,
             CBCharacteristicPropertyExtendedProperties                                        = 0x80,
             CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0)    = 0x100,
             CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0)    = 0x200
            */
            self.addInputString(str: "characteristic.properties --> \(characteristic.properties)")
            
            switch characteristic.properties {

            case CBCharacteristicProperties.write:
                self.addInputString(str: "characteristic ===> write")
                writeValue(characteristic) //写入数据
                tempCBCharacteristic = characteristic //给个全局的点,
                continue
            case CBCharacteristicProperties.writeWithoutResponse:
                self.addInputString(str: "characteristic ===> writeWithoutResponse")
                continue
            case CBCharacteristicProperties.read:
                self.addInputString(str: "characteristic ===> read")
                continue
            case CBCharacteristicProperties.notify:
                self.addInputString(str: "characteristic ===> notify")
                continue
            case CBCharacteristicProperties.indicate:
                self.addInputString(str: "characteristic ===> indicate") //获取本身的权限
                /*
                let f = UInt8(characteristic.properties.rawValue) & UInt8(CBCharacteristicProperties.write.rawValue)
                if f == CBCharacteristicProperties.write.rawValue { //判断本身有没有写的权限
                    self.addInputString(str: "characteristic ===> in indicate test write")
                    writeValue(characteristic) //写入数据
                    tempCBCharacteristic = characteristic //给个全局的点,
                }
                */
                continue
            case CBCharacteristicProperties.authenticatedSignedWrites:
                self.addInputString(str: "characteristic ===> authenticatedSignedWrites")
                continue
            case CBCharacteristicProperties.extendedProperties:
                self.addInputString(str: "characteristic ===> extendedProperties")
                continue
            case CBCharacteristicProperties.notifyEncryptionRequired:
                self.addInputString(str: "characteristic ===> notifyEncryptionRequired")
                continue
            case CBCharacteristicProperties.indicateEncryptionRequired:
                self.addInputString(str: "characteristic ===> indicateEncryptionRequired")
                
            default:
                self.addInputString(str: "characteristic ===> default")
                let f = UInt8(characteristic.properties.rawValue) & UInt8(CBCharacteristicProperties.write.rawValue)
            
                if f == CBCharacteristicProperties.write.rawValue { //判断本身有没有写的权限 这个可能是综合的 ---> 注意 16进制的转换问题~
                    self.addInputString(str: "characteristic ===> default --test-- write")
                    
                    tempCBCharacteristic = characteristic //给个全局的点,
                    self.addInputString(str: "连接成功,设置全局characteristic设置成功,可以发送数据")
                }
            }
        }
    }
    

ps: 这个里面的代码主要是做了一个判断,因为是demo,我全部都写上了,可以根据实际情况进行筛选,比方说只需要可以读的就显示一个.read就可以了~

注意:这里面的default操作,回调的characteristic.properties可能是一个复合信息,以位运算的形式返回,这里面使用了位操作判断是否支持读写。找到后保存了一个全局的characteristic。 最好将characteristic设置成接收notify,这样后面能接收到发送数据的回调信息。代码:peripheralSelected!.setNotifyValue(true, for: characteristic)

8.有了全局的characteristic 就可以发送信息了。

  func writeValue(_ Characteristic: CBCharacteristic) {
        
        let string = inputTextField.text ?? "~测试数据"
        let data = string.data(using: .utf8)
        self.addInputString(str: "写入测试数据 ==> ")
        peripheralSelected!.writeValue(data!, for: Characteristic, type: CBCharacteristicWriteType.withResponse)
    }

9.接收Notification 和 服务器回传的数据 :

// 获取外设发来的数据
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        print("接到服务端发送的数据")

        if (characteristic.value != nil) {
            print("开始解析数据")
            let str = String.init(data: characteristic.value!, encoding: .utf8)
            print(str)
            receiveMessage.text = receiveMessage.text + "\n" + (str ?? "~")
        }
    }
    //接收characteristic信息
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        print("接收characteristic信息")
    }

> 另一种连接方式:第三方库,这个库是OC写的。利用点语法,挺方便。

BabyBluetooth
SimpleCoreBluetooth (自己用swift封装的。)

如果作为服务端Server请参考
Android
iOS

本文代码

参考资料:
Swift语言iOS8的蓝牙Bluetooth解析
Pein_Ju的文章BLE蓝牙开发—Swift版

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