swift 5.0 蓝牙开发BLE 完整过程

5,603 阅读8分钟

基本概念(网上搜索的):

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

CoreBluetoothStructure.png

我的项目是手机作为中心设备。那么大概的流程就是这样的(说的不对请大佬指正):首先手机开启蓝牙后搜索外围设备,然后当连接外设的时候,扫描其特定的服务(CBService)以及服务下的特性(CBCharacteristic),这个CBCharacteristic非常重要,我们的读写等操作都是通过它来做处理的。后面就是进行数据的交互。

看到这,你可能感觉没什么难的,好的吧 。我就把我所遇到的问题和大家分享一下吧。

1.封装BLEToolManger.

刚刚开始了解了大致的流程后,我急急忙忙在某一个VC中写了很多代码,并且实现了蓝牙的功能,但是发现在后续的页面中,我还是要写一遍。这个问题就很严重了。1:代码混乱,难以调试。2:蓝牙外设的收发数据是有限制的,比如他不能同时接受多条指令,这样容易导致数据错乱。 为此参考了git上大佬的代码也封装了一个单例(也可以使用第三方库) 代码比较多,但是建议看看 代码如下重要参数已修改 注意咯

class TTBluetoothManger:NSObject {

    static var share =  TTBluetoothManger()
    private override init(){}
    
    lazy var centralManager:CBCentralManager = {
        let c =  CBCentralManager.init()
        c.delegate = self
        return c
    }()
    
    var connectedPeripheral:CBPeripheral?
    //发现的蓝牙外设
    var discoveredPeripheralsArr :[CBPeripheral?] = []
    var signalRSSIArr:[NSNumber?] = []

    //蓝牙加密认证 服务 CBCService的UUID
    let confirmServiceUUID = "0000FFF0-0000-1000-8000-XXXXXXXXXXX"
    //保存的设备特性char[3]
    var confirmCharacteristic : CBCharacteristic!
    //蓝牙加密认证 CBCharacteristic的UUID
    let confirmCharacteristicUUID = "0000FFF3-0000-1000-8000-XXXXXXXXXXX"
    //  char[4]
    var UUID_Char4Characteristic:CBCharacteristic!
    let UUID_NOTIFICATION = "0000FFF4-0000-1000-8000-XXXXXXXXXXX"
    //  char[6]
    var UUID_Char6Characteristic:CBCharacteristic!
    let UUID_WRITE = "0000FFF6-0000-1000-8000-XXXXXXXXXXX"
    //通知的描述
    var UUID_NOTIFICATION_DES2Descriptor:CBDescriptor!
    let UUID_NOTIFICATION_DES2 = "00002902-0000-1000-8000-XXXXXXXXXXX"
}

extension TTBluetoothManger{

    ///启用蓝牙,搜索链接设备
    ///在控制器中调用即可进行整个流程
    func bluetoohStar() {
        self.centralManager.delegate = self
        self.centralManager.scanForPeripherals(withServices: nil, options: nil)
    }
    ///连接外设
    func connect(peripheral: CBPeripheral) {
        self.connectedPeripheral = peripheral
        centralManager.connect(self.connectedPeripheral!, options: nil)
    }
    func cancelScan() {
        centralManager.stopScan()
    }
}
extension TTBluetoothManger:CBCentralManagerDelegate{
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .unknown:
            print("CBCentralManager state:", "unknown")
        case .resetting:
            print("CBCentralManager state:", "resetting")
        case .unsupported:
            print("CBCentralManager state:", "unsupported")
        case .unauthorized:
            print("CBCentralManager state:", "unauthorized")
        case .poweredOn:
            print("CBCentralManager state:", "poweredOn")
            ///扫描设备
            central.scanForPeripherals(withServices: nil, options: nil)
        case .poweredOff:
            print("CBCentralManager state:", "poweredOff")
        default:
            print("未知错误")
        }
    }
    ///发现设备
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        //过滤存在的蓝牙外设
        var isExisted = false
        for obtainedPeriphal  in discoveredPeripheralsArr {
            if (obtainedPeriphal?.identifier == peripheral.identifier){
                isExisted = true
                //更新信号轻度
                let index = discoveredPeripheralsArr.firstIndex(of: peripheral)!
                signalRSSIArr[index] = RSSI
            }
        }
        if !isExisted && peripheral.name != nil{
            discoveredPeripheralsArr.append(peripheral)
            signalRSSIArr.append(RSSI)
            
        }
        AUCNOTI.post(name: NOTIFICATION_DEVICE, object: self, userInfo: ["peripheral":discoveredPeripheralsArr,"rssi":signalRSSIArr])
    }
    ///连接设备成功
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        self.connectedPeripheral = peripheral
        peripheral.delegate = self
        //开始寻找Services。传入nil是寻找所有Services
        peripheral.discoverServices(nil)
    }
    ///连接设备失败
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        printShow(str: "连接失败:\(error.debugDescription)")
    }
    ///断开连接
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        printShow(str: "断开连接")
        ///可重新扫描
    }

}

extension TTBluetoothManger:CBPeripheralDelegate{
    
    ///寻找服务
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if (error != nil){
            
        }
        if let services = peripheral.services {
            for  service in services {
                
                if service.uuid.uuidString == confirmServiceUUID {
                    
                    peripheral.discoverCharacteristics(nil, for: service)
                }
                
            }
        }
    }

    /// 从感兴趣的服务中,确认 我们所发现感兴趣的特征
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {

        if error != nil{
            printShow(str: error?.localizedDescription)
        }  
        
        for characteristic in service.characteristics! {
            
            let propertie = characteristic.properties

            if propertie == CBCharacteristicProperties.notify {
                peripheral.setNotifyValue(true, for: characteristic)
                
            }
            if propertie == CBCharacteristicProperties.write {
                
            }
            if propertie == CBCharacteristicProperties.read {
                peripheral .readValue(for: characteristic)
            }
            
            //char[3]
            if characteristic.uuid.uuidString == confirmCharacteristicUUID {
                
                self.confirmCharacteristic = characteristic
                //写入
                let byte:[UInt8] = [0xAA]
                let data = Data(bytes: byte, count: 1)
                
                for byte in 0..<data.count {
                    printShow(str: "\(byte)")
                }
                self.connectedPeripheral!.writeValue(data, for: self.confirmCharacteristic, type: CBCharacteristicWriteType.withResponse)
                
                
            }
            //char[4]
            if characteristic.uuid.uuidString == UUID_NOTIFICATION {
                
                self.UUID_Char4Characteristic = characteristic
                
                if let descriptors = characteristic.descriptors {
                    
                    for descriptor in descriptors {
                        if descriptor.uuid.uuidString == UUID_NOTIFICATION_DES2 {
                            self.UUID_NOTIFICATION_DES2Descriptor = descriptor
                            
                        }
                    }
                }
                //设置char[4]的通知 来确定是否认证完成
                self.connectedPeripheral!.setNotifyValue(true, for: self.UUID_Char4Characteristic)
                
            }
            //char[6]
            if characteristic.uuid.uuidString == UUID_WRITE {
                
                self.UUID_Char6Characteristic = characteristic
//                self.connectedPeripheral!.setNotifyValue(true, for: self.UUID_Char6Characteristic)
                
                
            }
            
        }
    }
    
    //MARK: - 检测向外设写数据是否成功
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        
        if error != nil {
            
            printShow(str:error?.localizedDescription )
            AUCNOTI.post(name: NOTIFICATION_ISWRITE_SUCCESS, object: self, userInfo: ["writeEror":error as Any])
            
        }else{
            
        }
        
    }
    
    // 接收外设发来的数据 每当一个特征值定期更新或者发布一次时,我们都会收到通知;
    // 阅读并解译我们订阅的特征值
    // MARK: - 获取外设发来的数据
    // 注意,所有的,不管是 read , notify 的特征的值都是在这里读取
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {

        if error != nil {
            printShow(str: error?.localizedDescription)
        }
        
        
        if error == nil  && characteristic == UUID_Char4Characteristic{
            
            if let dataStr = CZModbus.convertDataBytes(toHex: characteristic.value) {
                if dataStr.hasPrefix("ff") {
                    let data = self.sendEncrpy(characteristic.value!)
                    connectedPeripheral?.writeValue(data, for: self.UUID_Char6Characteristic, type: .withResponse)
                }
            }
        }
        //判断是否认证成功
//
        if error == nil  && characteristic == UUID_Char4Characteristic{
            
            if let value = characteristic.value {

                if (CZModbus.convertDataBytes(toHex: value)!.hasPrefix("fe")) {
                    //跳转通知
                    AUCNOTI.post(name: NOTIFICATION_ISUPDATE_SUCCESS, object: self, userInfo: ["success":"","basic":value])
                }
                ///读
                if (CZModbus.convertDataBytes(toHex: value)!.hasPrefix("800003")) {
                   AUCNOTI.post(name: NOTIFICATION_ISUPDATE_SUCCESS, object: self, userInfo: ["basic":value])
                }
                ///写
                if (CZModbus.convertDataBytes(toHex: value)!.hasPrefix("800006")) {
                    AUCNOTI.post(name: NOTIFICATION_ISUPDATE_SUCCESS, object: self, userInfo: ["write":value])
//                    print("收到的数据")
//                    print(CZModbus.convertDataBytes(toHex: value))
//                    print("收到的数据")
                }
            }
        }
    }
    
    //接收characteristic信息    //MARK: - 特征的订阅状体发生变化
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
//        print("========特征的订阅状体变化========")
//        printShow(str: characteristic.uuid.uuidString)
        
    }
    
    ///加密方式
    ///手机写 0+ 认证计算后数据 到 char[6]
    func sendEncrpy(_ data:Data) -> Data {
        
        var tempData = Data.init(count: 9)
        for i in 1..<9 {
            tempData[i-1] = data[I]
            
        }
        let encrpyData = Encrypt.encrpy(tempData)
        
        var resultData = Data.init(count: 19)
        resultData[0] = 0xFF
        
        for i in 0..<16 {
            resultData[i+1] = encrpyData[I]
        }
        resultData[17] = 0x00;
        resultData[18] = 0x00;
        
        return resultData
    }
    
    ///发送指令
    /*
     * str------命令
     *type------哪一个页面的l命令
     **/
    func sendData(_ str:String,type:String) {

        
        if type == "Basic" {
            var dataStr = "80"
            dataStr+=str
            dataStr+="55"
            print(dataStr)
            if  let data = dataStr.hexData() {
                self.connectedPeripheral!.writeValue(data, for: self.UUID_Char6Characteristic, type: .withoutResponse)
            }
        }  
    }
    
    /*
     *处理CRC  CRC校验  10进制转16进制 
     *低字节在前  279a --> 9a27
     */
    func dealWithCRC(_ crc:String?)->String? {

        if let str = crc {
            let newCRC =  str.subString(start: 2, length: 2)+str.subString(start: 0, length: 2)
            
            return "\(CZModbus.convertHex(toDecimal: newCRC))"
        }
        return nil
    }
    
    //根据接收的数据返回带有CRC的Data
    func dealWithParam(_ param:[String:Data]) -> Data? {
        
        var bytes:[UInt8] = [UInt8]()
        var pos = 0
        
        var datas:Data!
        
        if let addressData = param["address"],let featureData = param["feature"] ,let commandData = param["command"] {
            
            bytes.append(addressData.first!)
            pos+=1
            bytes.append(featureData.first!)
            pos+=1
            
            datas = commandData
            
            for i in 0..<datas.count {
                bytes.append(datas[I])
                pos+=1
            }
            
            var data = Data()
            for i in 0..<pos {
                data.append(bytes[I])
            }
            var  crc = CZModbus.getCRC(data, isRead: false)
            
            
            crc = ((crc & 0xFF00) >> 8) | ((crc & 0x00FF) << 8)
            
            bytes.append(UInt8(((crc & 0xffff)>>8)))
            pos+=1
            bytes.append(UInt8((crc&0x0000)))
            pos+=1
            
            var data1:Data = Data()
            for i in 0..<pos {
                data1.append(bytes[I])
            }
            return data1
        }
        return nil
    }
}

上面这个单例就基本满足了我们。其中:confirmServiceUUID是我们查找目标设备的UUID,我们可以通过它来判断用户连接的设备是不是我们自己的外设。confirmCharacteristic这些就是我们进行操作的特征字啦。他们也都有UUID来标识。保存他们的目的是后续的读写操作方便。建议写代码之前先问一下硬件工程师,什么特征是干什么的,是否可写可读可以广播啥的,如果对方也不清楚,没关系,有它! WechatIMG15.jpeg WechatIMG16.jpeg

用手机去appStore 搜索一下 LightBlue 就可以看到你的外设拥有的一些服务以及特性啦。

2 蓝牙认证(iOS一般不多见吧)

蓝牙认证属于真正数据交互前的一个认证阶段,它大致的过程是这样的:当中心设备请求连接外设时,在某一个时间段内,完成认证,否则断开连接 给我的文档是这样子的,大家随便感受一下: WeChatd5f3a79c244432ae261be01b06fef430.png 各位您给看明白吗?我是不明白的😂 既然不明白,那我们在仔细阅读以下这个文档的内容。 1.手机写0xAA到char[3] 这一步我是明白的🤣 ,这一步就是简单的吧0xAA发给手机的char[3]这个特征值。代码是这样子的

                //写入
                let byte:[UInt8] = [0xAA]
                let data = Data(bytes: byte, count: 1)
                self.connectedPeripheral!.writeValue(data, for: self.confirmCharacteristic, type: CBCharacteristicWriteType.withResponse)

2.终端写0xFF+认证源数据 到char[4] 🤔这是啥咱们先不管 但是咱们看下一句 手机写0+认证计算后数据 到char[6] 这句就有点意思了,中心设备(手机)去写0+认证计算的数据,那就说明我们刚开始写入0xAA后,外设肯定给我们返回数据,我们对这个数据进行一系列的操作才可以变化为“认证计算后的数据”,总不能中心设备(手机)凭空制造认证数据吧。 总结: 终端写0xFF+认证源数据 到char[4] 后,我们去读取这个数据,进行加密等操作,并把它写入到char[6]中。 3. 终端验证手机计算的数据,并写0xFE到char[4]。 这句话的意思就是,我们发给外设的验证数据后,外设进行验证,如果是正确的,那么char[4]变化为0xFEXXX,如果不正确,那么就不会写入0xFE,这个也是我们进行判断我们的加密算法正确与否以及是否进行跳转或者进行其他逻辑操作的依据。

//加密的算法是根据安卓端写的。。大概长这样子(百度谷歌一堆)
import Foundation

class Encrypt {
    
   class func encrpy(_ data:Data) -> Data {
        let C1 = XXXXXX;//重要参数已修改!
        let C2 = KKKKK;//重要参数已修改!
        var Key = 0xllllllc;//重要参数已修改!
        var encrpyData = Data.init(count: 16)
        var tempData = data
        for i in 0..<8 { 
            tempData[i] = (data[i] & 0xFF) ^ UInt8((Key >> 8))
            Key = ((((Int)(tempData[i] & 0xFF)+Key)*C1+C2) & 0xFFFF )
        }
        for i in 0..<8 {
            encrpyData[i*2] = (tempData[i] & 0xFF)/26+65
            encrpyData[i*2+1] = (tempData[i] & 0xFF)%26+65
        }
        return encrpyData
    }
}

3 蓝牙数据的读取

经历了蓝牙认证后,我们就开始正常的读取蓝牙数据的操作, 这里我们就需要了解modBus协议和CRC认证。CRC是一种校验方法,比如我们发送的数据为[0x00,0x11],进行crc后变为[0x00,0x11,0x1e,0xff],后面的2为就是crc认证,我们收到蓝牙回传的数据 去掉crc 再次进行crc验证,得到的crc和返回的crc一致,那么数据是有效的(大致过程是这样子,详细请google) CRC算法大概是这样子的

+ (uint16_t) getCRC:(NSData *)data IsRead:(BOOL) isRead {
    uint16_t crc = 0xlllllllllllll; //参数已修改!!!!
    uint8_t ucCRCHi = 0xlllllllllllll;//参数已修改!!!
    uint8_t ucCRCLo = 0xlllllllllllll;//参数已修改!!!
    
    uint8_t iIndex;
    
    Byte *byteArray = (Byte *)[data bytes];
    
    if (isRead) {
        for (int i = 0; i < data.length - 2;i++) {
            iIndex = (ucCRCLo ^ byteArray[i]) & 0x00ff;
            ucCRCLo = ucCRCHi ^array_crc_low[iIndex];
            ucCRCHi = array_crc_high[iIndex];
        }
    }
    else {
        for (int i = 0; i < data.length;i++) {
            
            iIndex = (ucCRCLo ^ byteArray[i]) & 0x00ff;
            ucCRCLo = ucCRCHi ^array_crc_low[iIndex];
            ucCRCHi = array_crc_high[iIndex];
        }
    }
    
    crc = ((ucCRCHi & 0x00ff) << 8) | ((ucCRCLo & 0x00ff) & 0xffff);
    return  crc;
}

使用clayzhu大佬的工具并根据要求修改的代码

//具体页面发送获取参数指令的大致代码段 
    ///Send data 发送获取基本参数命令
    func sendBasicString() {
        
        //命令
        let  commandBytes:[UInt8] = [0x00,0x41,0x00,0x1C]
        let commandData = Data.init(bytes: commandBytes, count: commandBytes.count)
        //地址码
        let addressBytes:[UInt8] = [0x00]
        let addressData = Data.init(bytes: addressBytes, count: addressBytes.count)
        //查询码
        let featureBytes:[UInt8] = [0x03]
        let featureData = Data.init(bytes: featureBytes, count: featureBytes.count)

        var param = [String : Data]()
        param["address"] = addressData
        //功能码
        param["feature"] = featureData
        //查询码
        param["command"] = commandData
        
        
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
            if let data = self.bleManger.dealWithParam(param) {

                if let dataStr = CZModbus.convertDataBytes(toHex: data) {
                    self.bleManger.sendData(dataStr, type: "Basic")
                }
            }
        }
    }

4 向外设写入数据

#####写入数据不同于读数据,我们发送完写数据的指令后,如果数据格式合法,那么返回给我们的数据就是我们发送的数据。这里的主要问题是进制之间的转换(原谅我比较笨😭) 例如 var commandBytes:[UInt8] = [0x00,0x42],我准备在0x42写入65535怎么写,直接进行转换肯定是溢出的,这里还有一个问题就是,我使用[Int]来保存数据后转化为Data,发送给外设是没有反应的。具体原因不详。那我们就曲折一下:

        if let commandbyte = commandByte {
//10进制的数字转换为16进制字符串  66635 --->ffff
            let hex = String.init(commandbyte, radix: 16, uppercase: false)
            //16进制字符串转化为bytes数组([UInt8])
            if let data = CZModbus.convertHex(toDataBytes: hex) {
                for byte in data {
                    commandBytes.append(byte)
                }
            }
        }
这样就可以啦~~~~~
5 其他注意的问题:

1.防止多条命令同时写入。比如我们在某一个页面需要发送多条轮训命令查询设备参数,那我们就需要注意命令之间是否会导致接收数据异常的问题了。我的解决方式是我们在收到回传消息通知时,根据数据长度判断是A命令返回的数据,那么进行发送B命令,否则发送A命令。

2 写入命令时,暂停掉读数据命令。这里我们可以添加一个标志位来进行判断是否在进行写操作。

3 异常情况的处理。千万不要忘记异常情况的提示以及回复某些操作的逻辑代码

后记:从来没完整的学习过BLE的东西,稍微有点记忆的是很久之前的一次面试,面试官问我了解蓝牙的东西吗?如实回答后,感觉很是羞愧,回来也看了一些文章,但真的是纸上得来终觉浅,绝知此事要躬行。有些东西真的不是看看文章就会的,自己写这个文章也并不是要怎么样,就是单纯的想加深一下记忆,顺便看了下简书关于swift蓝牙的文章不是很多,希望对正在从事BLE开发的同事有一点帮助~~

QQ:350541732 希望结识更多学习RxSwift的童鞋,一起学习~~~