Basic Concepts (online search) :

CBCentralManager // System Bluetooth device management object

CBPeripheral // CBService // Services of the peripheral or services contained in the services CBCharacteristic // Service features CBDescriptor // Feature descriptors

My project is mobile phones as central devices. So the general process is like this: Firstly, the mobile phone searches for peripheral devices after bluetooth is turned on, and then scans its specific service (CBCharacteristic) and the characteristics under the service (CBCharacteristic) when connecting the peripheral devices. CBCharacteristic is very important, and our reading and writing operations are processed through it. Next comes the interaction of the data.

Now, you might think that’s not too hard. Okay. Let me share with you some of the problems I have encountered.

1. Wrap BLEToolManger.

After just starting to understand the general process, I rushed to write a lot of code in a VC and implemented Bluetooth, but found that I had to write it again in a later page. This is a serious problem. 1: The code is chaotic and difficult to debug. 2: The data receiving and receiving of Bluetooth peripherals are limited. For example, it cannot accept multiple instructions at the same time, which may easily lead to data confusion. For this reference git on the big guy code also packaged a singleton (can also use a third-party library) code more, but it is recommended to look at the code as follows important parameters have been modified note it

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
    }
}
Copy the code

This singleton above basically satisfies us. Among them:confirmServiceUUIDWe look up the UUID of the target device, which we can use to determine whether the device the user is connected to is our own peripheral.confirmCharacteristicThese are the feature words that we operate on. They are also identified by UUID. They are saved for easy subsequent read and write operations.It is recommended to ask the hardware engineer before writing the code, what features do what, can write, read and broadcast whatIf the other side is not clear, it doesn’t matter, have it!

Search for LightBlue in the appStore on your phone to see some of the services and features your peripheral has.

2 Bluetooth authentication (iOS is not common)

Bluetooth authentication is an authentication phase before real data interaction. The process of Bluetooth authentication is as follows: When the central device requests to connect to peripherals, the authentication is completed within a certain period of time; otherwise, the connection is disconnectedThe document I was given looked something like this, so you can feel it:Can you read me, gentlemen? I do not understand 😂 since I do not understand, we are carefully reading the content of the following document. 1.The phone writes 0xAA to char[3] 🤣. This step is simple. The code looks like this

// Write let byte:[UInt8] = [0xAA] let data = data (bytes: byte, count: 1) self.connectedPeripheral! .writeValue(data, for: self.confirmCharacteristic, type: CBCharacteristicWriteType.withResponse)Copy the code

2. The source data terminal write 0 XFF + certification to char [4] 🤔 this is what we regardless But let’s look at a mobile phone write 0 + certification calculated data to the char [6] this is a little mean, center equipment (mobile phone) to write 0 + certification calculation data, then we have just started writing 0 xaa, Peripherals must return data to us, and we can only change this data into “authenticated data after calculation” through a series of operations. It is impossible for the central equipment (mobile phone) to create authentication data out of thin air. Summary: After the terminal writes 0xFF+ authentication source data to CHAR [4], we read the data, encrypt it, and write it to char[6]. 3. The terminal verifies the data calculated by the phone and writes 0xFE to char[4]. If it is correct, char[4] will change to 0xFEXXX. If it is incorrect, 0xFE will not be written. This is also the basis for us to judge whether our encryption algorithm is correct and whether to jump or other logical operations.

// The encryption algorithm is based on android. Import Foundation class Encrypt {class func encrpy(_ data: data) -> data {let C1 = XXXXXX; Encrypt {class func encrpy(_ data: data) -> data {let C1 = XXXXXX; // Important parameters have been modified! let C2 = KKKKK; // Important parameters have been modified! var Key = 0xllllllc; // Important parameters have been modified! 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 } }Copy the code

3 Bluetooth data reading

After bluetooth authentication, we started to read Bluetooth data normally. Here, we need to understand modBus protocol and CRC authentication. CRC is a verification method. For example, the data we send is [0x00,0x11], which changes to [0x00,0x11, 0x1E, 0xFF] after CRC. The following 2 is CRC authentication. So the data is valid (this is the general process, please Google for details) and the CRC algorithm looks something like this

+ (uint16_t) getCRC:(NSData *)data IsRead:(BOOL) isRead { uint16_t crc = 0xlllllllllllll; // Parameters have been modified !!!! uint8_t ucCRCHi = 0xlllllllllllll; // Parameters have been modified!! uint8_t ucCRCLo = 0xlllllllllllll; // Parameters have been modified!! 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; }Copy the code

Use clayzhu’s tools and modify the code as required

Func sendBasicString() {// The let commandBytes:[UInt8] = command is used to fetch basic parameters Let commandData = data. init(bytes: commandBytes, count: Commandbytes.count) // Address code let addressBytes:[UInt8] = [0x00] let addressData = data. init(bytes: addressBytes, count: Addressbytes.count) // Query code let featureBytes:[UInt8] = [0x03] let featureData = data. init(bytes: featureBytes, count: featureBytes.count) var param = [String : Data]() param["address"] = addressData // Param ["feature"] = featureData // Query code 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") } } } }Copy the code

4 Writes data to the peripheral

##### Writing data is different from reading data. After we send the write data command, if the data format is valid, the data returned to us is the data we sent. For example, var commandBytes:[UInt8] = [0x00,0x42] I use [Int] to save Data, convert it to Data, and send it to peripherals with no response. The cause is unknown. Let’s take a twist:

If let commandbyte = commandbyte {// Let hex = String. Init (commandbyte, radix: 16, upperCase: false) // Convert hexadecimal string to bytes array ([UInt8]) if let data = czmodbus.converthex (toDataBytes: Hex) {for byte in data {commandbytes.append (byte)}} this can be used ~~~~~Copy the code
5 Other issues for attention:

1. Prevent multiple commands from being written simultaneously. For example, we need to send multiple rotation training commands to query device parameters on a certain page, so we need to pay attention to whether the commands will lead to abnormal received data. My solution is that when we receive the return message notification, we judge that the data is returned by command A according to the length of the data, so we send command B, or send command A.

2 Suspend the read data command when the command is being written. Here we can add a flag bit to determine whether a write operation is in progress.

3 Handling abnormal situations. Don't forget to alert you to exceptions and the logical code that responds to certain actions

Postscript: I have never learned BLE completely. What I remember a little is an interview long ago. The interviewer asked me if I knew anything about Bluetooth. Truthful answer, feel very ashamed, come back and read some articles, but really come zhongjue shallow paper, must know this to practice. There are some things that really can’t be learned by reading the article. I didn’t write this article for anything, just to deepen my memory. By the way, I read Jane’s book about Swift Bluetooth, which is not very many, and I hope it will be helpful to my colleagues who are engaged in BLE development

QQ:350541732 hope to meet more learning RxSwift children’s shoes, learn together ~~~