这几天小伙伴又给我提了一个新需求,要求给他们的项目提供一个iOS端的蓝牙通信App,虽然iOS也做过一些,Objective C和Swift多少也知道一些,不过并没有深入的研究过iOS的开发,为了节省工期囫囵吞枣的看了一些资料,像之前那篇文章一样,我这里就不舞大刀,我把我参考的资料就放在下面这里,你如果需要可以去看看。

《[iOS/swift]蓝牙连接》

Github代码仓库:MichaelLynx/BleDemo

《Swift CoreBluetooth 蓝牙库》

我参考这些资料和代码的基础上,重写了其中一部分代码,并且做了一定的封装,你可以直接复制黏贴,并且在自己的工程里使用。版本为Swift的,不使用Objective C是因为觉得写起来比较浪费时间,而且如果遇到了需要和底层数据打交到的地方,我依然可以用Objective C做封装就好了。

首先是这个BluetoothLowEnergy类的实现。

//
//  BluetoothLowEnergyHandler.swift
//  PetoiSerialSwift
//
//  Created by Orlando Chen on 2021/3/23.
//

import Foundation
import CoreBluetooth


class BluetoothLowEnergy: NSObject {
    
    // 一个蓝牙设备当中可能包含多个信道,一个UUID就是一个信道标记
    var uuids: [CBCharacteristic] = []
    
    // 中心对象
    var central : CBCentralManager!
    
    // 把中心设备扫描的外置设备保存起来
    var deviceList: [CBPeripheral] = []

    // 接收到的数据
    var peripheralData: Data?
    
    
    // MARK: 0. 初始化
    override init() {
        super.init()
        
        // 初始化中心设备管理器
        // 它的delegate函数为centralManagerDidUpdateState
        // 其回调消息处理函数为:centralManager
        self.central = CBCentralManager.init(delegate:self, queue:nil, options:[CBCentralManagerOptionShowPowerAlertKey:false])
        
        // 初始化设备列表
        self.deviceList = []
    }

    // MARK: 1. 扫描设备
    func startScanPeripheral(serviceUUIDS: [CBUUID]?,
                             options: [String: AnyObject]?) {
        // 清空列表
        deviceList = []  // 清空设备列表
        uuids = [] // 清空信道列表
        
        // 开始进行扫描
        self.central?.scanForPeripherals(withServices: serviceUUIDS, options: options)
    }
     
    // MARK: 2. 停止扫描
    func stopScanPeripheral() {
        self.central?.stopScan()
    }
    
    // MARK: 3.1. 获取搜索到的外接设备
    func getPeripheralList()-> [CBPeripheral] {
        return deviceList
    }
    
    // MARK: 3.2. 获取到当前蓝牙设备可用的消息信道
    func getCharacteristic() -> [CBCharacteristic] {
        return uuids
    }
    
    // MARK: 3.3. 指定监听信道
    func setNotifyCharacteristic(peripheral: CBPeripheral, notify: CBCharacteristic) {
        peripheral.setNotifyValue(true, for: notify)
    }
    
    // MARK: 4.1. 连结设备
    // 连接设备之前要先设置代理,正常情况,当第一次获取外设peripheral的时候就会同时设置代理
    func connect(peripheral: CBPeripheral) {
        if (peripheral.state != CBPeripheralState.connected) {
            central?.connect(peripheral , options: nil)
            
            // 将外接设备的回掉函数连结到self
            // 回掉消息处理函数为:peripheral
            peripheral.delegate = self
        }
    }
    
    // MARK: 4.2. 检测是否建立了连结
    func isConnected(peripheral: CBPeripheral) -> Bool {
        return peripheral.state == CBPeripheralState.connected
    }
    
    // MARK: 5.1. 发送数据
    func sendData(data: Data, peripheral: CBPeripheral, characteristic: CBCharacteristic,
                  type: CBCharacteristicWriteType = CBCharacteristicWriteType.withResponse) {
        
        let step = 20
        for index in stride(from: 0, to: data.count, by: step) {
            var len = data.count - index
            if len > step {
                len = step
            }
            let pData: Data = (data as NSData).subdata(with: NSRange(location: index, length: len))
            peripheral.writeValue(pData, for: characteristic, type: type)
        }
    }

    // MARK: 5.2. 接收数据
    func recvData() -> Data {
        return peripheralData ?? Data([0x00])
    }
    
    // MARK: 6. 断开连结
    func disconnect(peripheral: CBPeripheral) {
        central?.cancelPeripheralConnection(peripheral)
    }
}

extension BluetoothLowEnergy: CBCentralManagerDelegate {
    
    // MARK: 检查运行这个App的设备是不是支持BLE。
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
     
        switch central.state {
        case .poweredOn:
            NSLog("BLE poweredOn")
        case .poweredOff:
            NSLog("BLE powered off")
        case .unknown:
            NSLog("BLE unknown")
        case .resetting:
            NSLog("BLE ressetting")
        case .unsupported:
            NSLog("BLE unsupported")
        case .unauthorized:
            NSLog("BLE unauthorized")
        @unknown default:
            NSLog("BLE default")
        }
    }
    
    // MARK: 以ANCS协议请求的端,授权状态发生改变
    func centralManager(_ central: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral) {
//        NSLog("\(#file) \(#line) \(#function)\n central:\(central)\n peripheral:\(peripheral)")
        
        // TODO
    }
    
    // MARK: 状态的保存或者恢复
    func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
//        NSLog("\(#file) \(#line) \(#function)\n central:\(central)\n peripheral:\(dict)")
        
        // TODO
    }
    
    // MARK:
    func centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) {
//        NSLog("\(#file) \(#line) \(#function)\n central:\(central)\n  peripheral:\(peripheral)")
        
        // TODO
    }
    
    // 开始扫描之后会扫描到蓝牙设备,扫描到之后走到这个代理方法
    // MARK: 中心管理器扫描到了设备
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        
//        NSLog("\(#file) \(#line) \(#function)\n central:\(central)\n peripheral:\(peripheral)")
        
        guard !deviceList.contains(peripheral), let deviceName = peripheral.name, deviceName.count > 0 else {
            return
        }
        
        // 把设备加入到列表中
        deviceList.append(peripheral)
        
        // TODO
    }
       
    // MARK: 连接外设成功,开始发现服务
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
//        NSLog("\(#file) \(#line) \(#function)\n central:\(central)\n peripheral:\(peripheral)")
        
         // 设置代理
         peripheral.delegate = self
         
         // 开始发现服务
         peripheral.discoverServices(nil)
    }

       
    // MARK: 连接外设失败
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral:
                            CBPeripheral, error: Error?) {
//        NSLog("\(#file) \(#line) \(#function)\n central:\(central)\n peripheral:\(String(describing: peripheral.name))\n error:\(String(describing: error))")
        
        // TODO
    }
       
    // MARK: 连接丢失
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        
//        NSLog("\(#file) \(#line) \(#function)\n central:\(central)\n peripheral:\(String(describing: peripheral.name))\n  error:\(String(describing: error))")
        
       // TODO
    }
}


// MARK: 外置设备被绑定后的事件响应
extension BluetoothLowEnergy: CBPeripheralDelegate {
    
    // MARK: 匹配对应服务UUID
    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverServices error: Error?) {
        
        if error != nil { // failed
//            NSLog("\(#file) \(#line) \(#function)\n peripheral:\(String(describing: peripheral.name))\n error:\(String(describing: error))")
            return
        }

//        NSLog("\(#file) \(#line) \(#function)\n peripheral:\(String(describing: peripheral.name))")
        
        
        for service in peripheral.services ?? [] {
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }
    
    
    // MARK: 服务下的特征
    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverCharacteristicsFor service:
                        CBService, error: Error?) {
        
        if error != nil { // failed
//            NSLog("\(#file) \(#line) \(#function)\n peripheral:\(String(describing: peripheral.name))\n service:\(String(describing: service))\n error:\(String(describing: error))")
            return
        }
        
//        NSLog("\(#file) \(#line) \(#function)\n peripheral:\(String(describing: peripheral.name))\n service:\(String(describing: service))")
          
        for characteristic in service.characteristics ?? [] {
            uuids.append(characteristic)
        }
    }

    
    // MARK: 获取外设发来的数据
    // 注意,所有的,不管是 read , notify 的特征的值都是在这里读取
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        
        if error != nil {
//            NSLog("\(#file) \(#line) \(#function)\n peripheral:\(String(describing: peripheral.name))\n characteristic:\(String(describing: characteristic.description))\n error:\(String(describing: error))")
            return
        }
        
//        NSLog("\(#file) \(#line) \(#function)\n peripheral:\(String(describing: peripheral.name))\n characteristic:\(String(describing: characteristic.description))")
        
        if let data = characteristic.value {
            self.peripheralData = data
        }
    }
    
    //MARK: 检测中心向外设写数据是否成功
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        if let error = error {
//            NSLog("\(#file) \(#line) \(#function)\n peripheral:\(String(describing: peripheral.name))\n characteristic:\(String(describing: characteristic.description))\n error:\(String(describing: error))")
        }
        
        // TODO
    }
}

所有的NSlog都是可以根据需要打开,方便调试使用的。TODO部分是你可以在上面进一步扩展的地方,当然修改原代码也是可以的。

至于如何使用?

首先,在iOS工程需要先在plist增加

Privacy - Bluetooth Always Usage Description

然后在某个类里实例化这个BLE类,如果是全局都需要进行蓝牙数据传输,那么就把这个类实例在AppDelegate文件里,并且设置为静态,这样就可以全局使用了。

用户需要具体调用的几个函数如下:

// MARK: 1. 扫描设备
func startScanPeripheral(serviceUUIDS: [CBUUID]?, options: [String: AnyObject]?) 
     
// MARK: 2. 停止扫描
func stopScanPeripheral()
    
// MARK: 3. 获取搜索到的外接设备
func getPeripheralList()-> [CBPeripheral] 
    
// MARK: 4.1. 连结设备
func connect(peripheral: CBPeripheral)
    
// MARK: 4.2. 检测是否建立了连结
func isConnected(peripheral: CBPeripheral) -> Bool

// MARK: 4.3. 获取到当前蓝牙设备可用的消息信道
func getCharacteristic() -> [CBCharacteristic]

// MARK: 4.4. 指定监听信道
func setNotifyCharacteristic(peripheral: CBPeripheral, notify: CBCharacteristic)
    
// MARK: 5.1. 发送数据
func sendData(data: Data, peripheral: CBPeripheral, characteristic: CBCharacteristic, type: CBCharacteristicWriteType = CBCharacteristicWriteType.withResponse) 

// MARK: 5.2. 接收数据
func recvData() -> Data 
    
// MARK: 6. 断开连结
func disconnect(peripheral: CBPeripheral)

由于蓝牙设备的通信过程是一个异步过程,等价于创建了一个后台线程对数据服务进行监听,所以如果对于音频流这一类实时会接受大量数据的应用,你可能需要增加一个标记或者其他方式以便处理,具体处理函数为

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)

只不对对于文本数据,这个代码已经足够使用了,比较简单的一种处理方法,就是用个定时器,每隔几十毫秒查询一次数据;而比较实时的处理方法,就是做一个回调函数,不过这些都由你自己决定怎么做吧,好运!