iOS 蓝牙模块 Core Bluetooth 简介(Swift 版)

一、Core Bluetooth 简介

蓝牙 是一项迅速发展的技术,Core Bluetooth 是苹果 提供的蓝牙低功耗(BLE)设备通信应用框架。应用程序可以通过它发现、搜索低功耗的外围设备并与之交互。该框架是蓝牙 4.0 规范中关于使用低功耗设备的抽象。

当使用 Core Bluetooth 并不是处理如无线扬声器那样的经典蓝牙设备。与这类设备的通讯会很快的耗尽电池能量。Core Bluetooth 是针对“Bluetooth Low Energy”(BLE)的 API,也称为“Bluetooth 4.0”。BLE 使用的电力要少得多,因为它的设计目的是通信少量的数据。

1.1 The Central(中央设备)

中央设备是硬件/软件的一部分,就像 iPhone、iPad、MacBook、iMac 等,其实我们可以直接理解成 手机。这些设备可以使用应用程序扫描像 心率监测器(HRM) 这样的蓝牙外围设备。中央设备是一个客户以及消费者。它们与 HRM 是连通的,所以它们可以使用从外围设备中取出的像每分钟心跳、电池的电量水平、还有 “RR-Interval” 这样的数据。中央设备接收这些数据,可以对数据执行增值计算,或者只是通过用户界面显示数据,或者是存储数据以供将来分析、展示,或者是聚合和数据分析(就像统计分析需要足够的数据来确定重要的和有意义的趋势),或其他类似的操作。

CBCentralManagerDelegate:协议定义了方法,CBCentralManager 对象的代理必须遵守它。协议中的可选方法允许代理来监控对外围设备的发现、连接、还有检索。唯一必须实现的方法表明中央设备的可用性,并且当中央设备的状态发生更新时被调用。

1.2 The Peripheral(外围设备)

外围设备是硬件/软件的一部分,就像 HRM。大多数 HRM 设备搜集或/和计算数据,如每分钟心跳、HRM 的电池电量水平、以及所谓的“RR-Interval”。设备传输这些数据到另一个需要它们的实体或实体组。外围设备是服务者和生产者。

CBPeripheral 对象的代理必须遵守 CBPeripheralDelegate 协议。代理使用这个协议的方法来对一个远程外围设备的服务和属性,进行发现、探索、还有交互方面的监控。这个协议里面没有必须遵守的方法。

1.3 通过广播找到外围设备

外围设备以广播包的形式广播一些数据。一个广播包是一个相对较小的数据束,其中可能包含外围设备所能提供的有用信息,比如外围设备的名字还有主要功能。例如,数字恒温器可能会广播它能提供房间的当前温度。在 BLE 中,广播是外围设备展示其存在的主要方式。另一方面,中央设备可以扫描和监听任何外围设备,只要这些设备的广播信息是它感兴趣的……


1.4 外围设备的各种服务

服务是一个数据和相关行为的集合,用于实现设备(或设备的一部分)的功能或特性。比如,心率检测器的一项服务可能是公开来自监测器的心率传感器的心率数据。服务可能不是你认为的那样。服务描述外围设备提供的主要特性或功能。但它并不是一种具体的测量方法,如每分钟心跳数,而是一种描述从外围设备可以得到的与心脏相关的测量方法的分类。



具体定义一个蓝牙“服务”,应该看看 Bluetooth SIG 的 “GATT Services(服务)” 列表,这里 GATT 代表 “Generic Attributes(通用属性)”。

向下滚动服务 列表,直到你在 Name(名字) 列中看到 “Heart Rate”。注意, Uniform Type Identifier (统一类型标识符) 对应的是 org.bluetooth.service.heart_rate”,Assigned Number(指定编码) 则是 0x180D。请注意在后面的代码中我们将使用 0x180D 这个值。
点击 “Heart Rate(心率)”,你将打开一个网页,上面用粗体字写着 Name: Heart Rate。请注意 Summary(摘要) ,“HEART RATE Service(心率服务)公开心率和其他与心率传感器相关的数据,用于健身应用。”向下滚动页面就会发现 Heart Rate service 本身并不会提供每分钟跳动的实际心率。这个服务是一个其他数据片段的集合,它们被称为 characteristics(特征)。最后,你会得到一个特征来提供重要数据:心率。

CBService 和它的子类 CBMutableService 代表一个外围设备的服务 - 为实现设备(或设备的一部分)的功能或特性而收集的数据和相关行为。CBService 对象特指远程外围设备(使用 CBPeripheral 对象来表示)的服务。服务组可能是主要的,也有可能是次要的,可能会包含一个特征组的代码,也有可能会包含一个服务组(代表其他的服务组)。

1.5 外围设备服务的特征

外围设备的服务常常被分解成更细化但相关的信息。特征通常是我们找到重要信息、真实数据的地方。

服务本身是由特征或包含的服务(这里指别的服务)组成。特征更详细的提供了外围设备的服务信息。例如,刚才描述的心率服务,可能包含一个描述设备的心率传感器所在目标身体位置的特征和另一个传递心率测量数据的特征。

让我们继续使用 HRM 作为例子。请返回那个用粗体字写着 Name: Heart Rate(名字:心率) 的 界面。向下滚动直到你看到 Service Characteristics(服务特征)。那是一个包含大量元数据(关于信息的数据)的大表格。请找到 Heart Rate Measurement(心率测量) 并点击 org.bluetooth.characteristic.heart_rate_measurement 然后审查。


CBCharacteristic 和它的子类 CBMutableCharacteristic 代表关于外围设备服务的详细信息。CBCharacteristic 对象特指远程外围设备(远程外围设备使用 CBPeripheral 对象表示)服务的特征。一个特征包含一个单一的值以及任意个描述符来描述这个值。特征的属性描述了如何使用这个特征的值以及如何访问这些描述符。

二、应用示例

在编写应用程序,将 Core Bluetooth 组件分解成协议或类,将核心功能从 UI 中分离出来。

编写 Core Bluetooth 代码
在这次讨论中,我将假设你了解 iOS 应用程序开发的基础知识,包括 Swift 编程语言和 Xcode Single View App(单视图应用程序)模板。测试应用程序的用户界面(UI),包括 Auto Layout(自动布局),代码如下所示,非常简单。
我将用一系列步骤来描述代码 — 这些步骤在下面的代码中同样会被解释。因此,在阅读本节中的步骤时,请参阅下面代码中对应的步骤。整个过程基本上是线性的。请记住,其中一些步骤表示回调 — 正在调用的委托方法。
在编写应用程序时,我会将 Core Bluetooth 组件分解成协议或类 — 例如,将核心功能从 UI 中分离出来。但这段代码的目的是向您展示 Core Bluetooth 如何在最少的干扰下工作。我的注释很简单,而且有实际意义。在一个页面中你只会看到重要部分。

2.1 逐步了解代码

  • Step 0.00 : 导入 CoreBluetooth 框架。
  • Step 0.0 : 指定 GATT 中的 Assigned Numbers(分配符) 为常量。这样做让蓝牙规范的标识符更具可读性和可维护性,针对 “心率” 服务,其 “心率测量” 特征,还有其 “身体传感器位置” 特征。
  • Step 0.1 : 创建一个 UIViewController 的子类 HeartRateMonitorViewController。使 HeartRateMonitorViewController 遵守 CBCentralManagerDelegateCBPeripheralDelegate 协议。使用协议和委托的设计模式,将实现来自两个协议的方法。调用一些 Core Bluetooth 的方法,一些方法将由 Core Bluetooth 调用,以响应自己的调用。
  • Step 0.2 : 在 HeartRateMonitorViewController 类中定义实例变量,它们代表 CBCentralManager 和 CBPeripheral 类,所以它们在应用程序的生命周期内都是持续存在的。

  • Step 1 : 为进程在后台创建一个并发队列。希望 Core Bluetooth 的运行发生在后台。且 UI 保持响应。在一个更复杂的应用程序中,HRM 可能会运行数小时,为用户收集心率数据。用户可能希望使用其他应用程序特性,例如,修改应用程序设置,或者,如果用户正在跑步,并且希望使用 Core Location 来跟踪跑步的路线。因此,在心率数据正在收集和显示的同时,用户可以收集和/或查看他们的地理位置。

  • Step 2 : 创建用于扫描、连接、管理和从外围设备收集数据的控制中心。这是必要的一步。缺少了控制中心 Core Bluetooth 将无法工作。另一个必要的:由于 HeartRateMonitorViewController 采用了 CBCentralManagerDelegate,将 centralManager 的委托属性设置成 HeartRateMonitorViewController(self)。同时还为控制中心指定了 DispatchQueue。

  • Step 3.1 : centralManagerDidUpdateState 方法的调用基于设备的蓝牙状态。理想情况下,应该考虑一个场景,在该场景中,用户无意(或故意)在 Settings(设置) 应用程序中关闭蓝牙。我们只能在蓝牙为 .poweredOn 状态时才能扫描外围设备。

  • Step 3.2 : 控制中心应该扫描感兴趣的外围设备,但前提是设备(如iPhone)开启了蓝牙。监听只针对正在广播 心率 服务(0x180D)的 HRM。可以通过添加特定服务的 CBUUIDs 到 serviceUUIDs 数组参数(标记为 withServices),从而达到监听并且连接更多外围设备的目的。例如,在一些健康相关的应用程序中,可以监听并连接到 HRM 和血压监测器或者 BPM(尽管我们需要再创建一个 CBPeripheral 类的实例变量)。注意,如果我们做了这个调用:
    centralManager?.scanForPeripherals(withServices: nil)

可以监听范围内所有蓝牙设备的广播。在一些蓝牙功能类的应用程序中它可能有用。

  • Step 4.1 : 找到这个应用程序可以连接哪些感兴趣的外围设备(HRM)。这个 didDiscover 方法告诉我们,在扫描时,控制中心已经发现了正在广播的 HRM。
  • Step 4.2 : 必须在类的实例变量中保存刚刚发现的外围设备的引用,它将持续存在。
  • Step 4.3 : 因为 HeartRateMonitorViewController 采用了 CBPeripheralDelegate 协议,所以 peripheralHeartRateMonitor 对象必须将它的 delegate 属性设置为 HeartRateMonitorViewController(self)。

  • Step 5 : 在 didDiscover 中告诉控制中心停止扫描以便保护电池寿命。当已经连接的 HRM 或外围设备断开连接时,我们可以再次开启扫描。

  • Step 6 : 此时还在 didDiscover 中,我们连接到被发现的感兴趣的外围设备,一个 HRM。
    centralManager?.connect(peripheralHeartRateMonitor!)
  • Step 7 : didConnect 方法仅仅 “当成功与一个外围设备连接时调用” 请注意 “成功” 这个词。如果发现一个外围设备但不能连接,那么你需要进行一些调试。请注意更新了 UI 用来显示连接了那个外围设备,并表明已经停止扫描,以及其他一些事情。
  • Step 8 : 此时还在 didConnect 方法中,在外围设备上寻找感兴趣的服务。具体来说,我们希望找到 Heart Rate(心率)(0x180D)服务。
    peripheralHeartRateMonitor?.discoverServices([BLE_Heart_Rate_Service_CBUUID])
  • Step 9 : 当 didDiscoverServices 方法被调用的时候,说明在我们所连接的外围设备上发现了 “Heart Rate(心率)” 服务。请记住我们需要寻找感兴趣的特征。这里对 Heart Rate(心率) 服务的所有特征进行了一次循环以找到接下来要用的那个。如果前往 Bluetooth SIG 网页中 “Heart Rate(心率)” 服务对应的页面,滚动到下面标记为 Service Characteristics(服务特征) 的分区,就可以查看那三个可用的特征。
    peripheral.discoverCharacteristics(nil, for: service)
  • Step 10 : didDiscoverCharacteristicsFor service 方法证明我们已经发现了感兴趣的服务中所有的特征。
  • Step 11 : 首先,订阅了一个通知 - “read” - 关于感兴趣的 Body Sensor Location(传感器所在身体部位) 特征。前往 “Heart Rate(心率)” 服务的页面,会发现这个特征被标记为“Read Mandatory。” 调用 peripheral.readValue 将会引起 peripheral:didUpdateValueForCharacteristic:error: 方法稍后被调用,所以可以将这个特征解析成人类语言。其次,我订阅了一个定期通知 — “notify” — 关于感兴趣的 Heart Rate Measurement(心率测量) 特征。前往 “Heart Rate(心率)” 服务的页面,你会发现这个特征被标记为“Notify Mandatory。”调用 peripheral.setNotifyValue 将会引起 peripheral:didUpdateValueForCharacteristic:error: 方法稍后被调用,并且是几乎每一秒钟触发一次,所以可以将这个特征解析成人类语言。
    //
    // Read    Mandatory
    //
    peripheral.readValue(for: characteristic)
    //
    // Notify    Mandatory
    //
    peripheral.setNotifyValue(true, for: characteristic)
  • Step 12 : 因为对特征 Body Sensor Location(传感器所在身体部位) (0x2A38)订阅了读取值,并且对特征 Heart Rate Measurement(心率测量) (0x2A37)订阅了定期获取通知,所以如果它们发送值或者定期更新,将分别获得这两个二进制值。
  • Step 13 : 将 BLE Heart Rate Measurement(心率测量) 的数据解译成人们可读的格式。前往 GATT 规范的 页面 找到这个特征。第一个字节是关于其余数据的元数据 (标记)。规范定义第一个字节的最低有效位,Heart Rate Value Format bit(心率值的标识位)。如果是 0(zero),每分钟的心跳数将以 UINT8 格式在第二字节。这就是为什么忽略了 Heart Rate Value Format bit(心率值的标识位) 值为 1(one)的用例。我看过所有被提到的实现,但从来没有能够测试这些实现。
  • Step 14 : 将 BLE Body Sensor Location(传感器所在身体部位) 的数据解译成人们可读的格式。前往 GATT 规范的 页面 找到这个特征。这个特征非常简单。将值 1、2、3、4、5、6 或 7 存储在 8 位中。形成的文本字符串与这些值以解译为目的的展示是一样的。
    let heartRateValue = heartRateMeasurementCharacteristic.value!
    // 转换为无符号 8 位整数数组
    let buffer = [UInt8](heartRateValue)
    let sensorLocationValue = sensorLocationCharacteristic.value!
    //  转换为无符号 8 位整数数组
    let buffer = [UInt8](sensorLocationValue)
  • Step 15 : 当一个外围设备从控制中心断开时,采取适当的行动。更新 UI 以及……
  • Step 16 : 开始扫描,为了发现一个正在广播 Heart Rate(心率) 服务(0x180D)的外围设备。
    // STEP 16: 在这个用例中,开始扫描相同或其他的外设,只要它们是 HRM,就可以重新联机
    centralManager?.scanForPeripherals(withServices: [BLE_Heart_Rate_Service_CBUUID])

2.2 源代码

这里是对于上这所讨论的实现,完整的源代码:

import UIKit

// STEP 0.00: 必须导入 CoreBluetooth framework
import CoreBluetooth

// STEP 0.0: 指定 GATT 中的 "Assigned Numbers" 为常量,这样它们会拥有更好的可读性和可维护性

// MARK: - Core Bluetooth 服务 ID
let BLE_Heart_Rate_Service_CBUUID = CBUUID(string: "0x180D")

// MARK: - Core Bluetooth 特征 ID
let BLE_Heart_Rate_Measurement_Characteristic_CBUUID = CBUUID(string: "0x2A37")
let BLE_Body_Sensor_Location_Characteristic_CBUUID = CBUUID(string: "0x2A38")

// STEP 0.1: 这个类同时采用了控制中心和外围设备的委托协议,所以必须遵守这些协议的要求
class HeartRateMonitorViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {

    // MARK: - Core Bluetooth 类的成员变量

    // STEP 0.2: 分别创建 CBCentralManager 和 CBPeripheral 的实例变量
    // 所以它们在应用程序的生命周期里持续存在
    var centralManager: CBCentralManager?
    var peripheralHeartRateMonitor: CBPeripheral?

    // MARK: - UI outlets / 成员变量

    @IBOutlet weak var connectingActivityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var connectionStatusView: UIView!
    @IBOutlet weak var brandNameTextField: UITextField!
    @IBOutlet weak var sensorLocationTextField: UITextField!
    @IBOutlet weak var beatsPerMinuteLabel: UILabel!
    @IBOutlet weak var bluetoothOffLabel: UILabel!

    // 设置 HealthKit 
    let healthKitInterface = HealthKitInterface()

    // MARK: - UIViewController delegate
    override func viewDidLoad() {
        super.viewDidLoad()
        // 在视图加载完成以后,通常是通过一个 nib,做所有附加的设置。

        // 最初,在进行扫描并且没有产生连接
        connectingActivityIndicator.backgroundColor = UIColor.white
        connectingActivityIndicator.startAnimating()
        connectionStatusView.backgroundColor = UIColor.red
        brandNameTextField.text = "----"
        sensorLocationTextField.text = "----"
        beatsPerMinuteLabel.text = "---"
        // 以防 Bluetooth 被关闭
        bluetoothOffLabel.alpha = 0.0

        // STEP 1: 为控制中心在后台创建一个并发队列
        let centralQueue: DispatchQueue = DispatchQueue(label: "com.iosbrain.centralQueueName", attributes: .concurrent)
        // STEP 2: 创建用于扫描、连接、管理和从外围设备收集数据的控制中心。
        centralManager = CBCentralManager(delegate: self, queue: centralQueue)

        // 从 HKHealthStore 读取心率数据
        // healthKitInterface.readHeartRateData()

        // 从 HKHealthStore 读取性别类型
        // healthKitInterface.readGenderType()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // 处理任何可以重新创建的资源
    }


    // MARK: - CBCentralManagerDelegate methods
    // STEP 3.1: 这个方法的调用基于设备的蓝牙状态; 
    // 仅在 Bluetooth 为 .poweredOn 时才可以扫描外围设备
    func centralManagerDidUpdateState(_ central: CBCentralManager) {

        switch central.state {

        case .unknown:
            print("Bluetooth status is UNKNOWN")
            bluetoothOffLabel.alpha = 1.0
        case .resetting:
            print("Bluetooth status is RESETTING")
            bluetoothOffLabel.alpha = 1.0
        case .unsupported:
            print("Bluetooth status is UNSUPPORTED")
            bluetoothOffLabel.alpha = 1.0
        case .unauthorized:
            print("Bluetooth status is UNAUTHORIZED")
            bluetoothOffLabel.alpha = 1.0
        case .poweredOff:
            print("Bluetooth status is POWERED OFF")
            bluetoothOffLabel.alpha = 1.0
        case .poweredOn:
            print("Bluetooth status is POWERED ON")

            DispatchQueue.main.async { () -> Void in
                self.bluetoothOffLabel.alpha = 0.0
                self.connectingActivityIndicator.startAnimating()
            }

            // STEP 3.2: 扫描我们感兴趣的外围设备
            centralManager?.scanForPeripherals(withServices: [BLE_Heart_Rate_Service_CBUUID])
        } // END switch
    } // END func centralManagerDidUpdateState


    // STEP 4.1: 找到这个应用程序可以连接哪些感兴趣的外围设备
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {

        print(peripheral.name!)
        decodePeripheralState(peripheralState: peripheral.state)
        // STEP 4.2: 必须储存一个外围设备的引用到类的实例变量中
        peripheralHeartRateMonitor = peripheral
        // STEP 4.3: 因为 HeartRateMonitorViewController 采用了 CBPeripheralDelegate 协议,
        // 所以 peripheralHeartRateMonitor 必须设置他的 
        // delegate 属性为 HeartRateMonitorViewController (self)
        peripheralHeartRateMonitor?.delegate = self

        // STEP 5: 停止扫描以保护电池的寿命;当断开链接的时候再次扫描。
        centralManager?.stopScan()

        // STEP 6: 与已经发现的,感兴趣的外围设备建立连接
        centralManager?.connect(peripheralHeartRateMonitor!)
    } // END func centralManager(... didDiscover peripheral


    // STEP 7: “当一个与外围设备的连接被成功创建时调用。”
    // 只有当我们知道与外围设备的连接建立成功之后才能前往下一步
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {

        DispatchQueue.main.async { () -> Void in
            self.brandNameTextField.text = peripheral.name!
            self.connectionStatusView.backgroundColor = UIColor.green
            self.beatsPerMinuteLabel.text = "---"
            self.sensorLocationTextField.text = "----"
            self.connectingActivityIndicator.stopAnimating()
        }

        // STEP 8: 在外围设备上寻找感兴趣的服务
        peripheralHeartRateMonitor?.discoverServices([BLE_Heart_Rate_Service_CBUUID])
    } // END func centralManager(... didConnect peripheral


    // STEP 15: 当一个外围设备断开连接,使用适当的方法
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {

        // print("Disconnected!")
        DispatchQueue.main.async { () -> Void in

            self.brandNameTextField.text = "----"
            self.connectionStatusView.backgroundColor = UIColor.red
            self.beatsPerMinuteLabel.text = "---"
            self.sensorLocationTextField.text = "----"
            self.connectingActivityIndicator.startAnimating()
        }

        // STEP 16: 在这个用例中,开始扫描相同或其他的外设,只要它们是 HRM,就可以重新联机
        centralManager?.scanForPeripherals(withServices: [BLE_Heart_Rate_Service_CBUUID])
    } // END func centralManager(... didDisconnectPeripheral peripheral


    // MARK: - CBPeripheralDelegate methods
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {

        for service in peripheral.services! {

            if service.uuid == BLE_Heart_Rate_Service_CBUUID {

                print("Service: \(service)")

                // STEP 9: 在感兴趣的服务中寻找感兴趣的特征
                peripheral.discoverCharacteristics(nil, for: service)
            }
        }
    } // END func peripheral(... didDiscoverServices


    // STEP 10: 从感兴趣的服务中,确认所发现感兴趣的特征
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        for characteristic in service.characteristics! {
            print(characteristic)
            if characteristic.uuid == BLE_Body_Sensor_Location_Characteristic_CBUUID {
                // STEP 11: 订阅关于感兴趣特征的单次通知;
                // “当你使用这个方法去读取特征的值时,外围设备将会调用…… 
                // peripheral:didUpdateValueForCharacteristic:error:”
                //
                // Read    Mandatory
                //
                peripheral.readValue(for: characteristic)
            }

            if characteristic.uuid == BLE_Heart_Rate_Measurement_Characteristic_CBUUID {

                // STEP 11: 订阅关于感兴趣特征的持续通知;
                // “当你启用特征值的通知时,外围设备调用……
                // peripheral(_:didUpdateValueFor:error:)” 
                //
                // Notify    Mandatory
                //
                peripheral.setNotifyValue(true, for: characteristic)
            }
        } // END for
    } // END func peripheral(... didDiscoverCharacteristicsFor service


    // STEP 12: 每当一个特征值定期更新或者发布一次时,我们都会收到通知;
    // 阅读并解译订阅的特征值
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if characteristic.uuid == BLE_Heart_Rate_Measurement_Characteristic_CBUUID {
            // STEP 13: 通常需要将 BLE 的数据解析成人类可读的格式
            let heartRate = deriveBeatsPerMinute(using: characteristic)
            DispatchQueue.main.async { () -> Void in
                UIView.animate(withDuration: 1.0, animations: {
                    self.beatsPerMinuteLabel.alpha = 1.0
                    self.beatsPerMinuteLabel.text = String(heartRate)
                }, completion: { (true) in
                    self.beatsPerMinuteLabel.alpha = 0.0
                })
            } // END DispatchQueue.main.async...
        } // END if characteristic.uuid ==...

        if characteristic.uuid == BLE_Body_Sensor_Location_Characteristic_CBUUID {
            // STEP 14: 通常需要将 BLE 的数据解析成人类可读的格式
            let sensorLocation = readSensorLocation(using: characteristic)

            DispatchQueue.main.async { () -> Void in
                self.sensorLocationTextField.text = sensorLocation
            }
        } // END if characteristic.uuid ==...
    } // END func peripheral(... didUpdateValueFor characteristic


    // MARK: - Utilities
    func deriveBeatsPerMinute(using heartRateMeasurementCharacteristic: CBCharacteristic) -> Int {

        let heartRateValue = heartRateMeasurementCharacteristic.value!
        // 转换为无符号 8 位整数数组
        let buffer = [UInt8](heartRateValue)

        // UInt8: “一个 8 位无符号整数类型。”

        // 在缓冲区的第一个字节(8 位)是标记(元数据,用于管理包中其余部分);
        // 如果最低有效位(LSB)是 0,心率(bpm)则是 UInt8 格式,
        // 如果 LSB 是 1,BPM 则是 UInt16
        if ((buffer[0] & 0x01) == 0) {
            // 第二个字节:“心率的格式被设置为 UINT8”
            print("BPM is UInt8")
            // 将心率写入 HKHealthStore
            // healthKitInterface.writeHeartRateData(heartRate: Int(buffer[1]))
            return Int(buffer[1])
        } else { // 我从来没有看到过这个用例,所以我把它留给理论学家去争论
            // 第二个和第三个字节:“心率的格式被设置为 UINT16”
            print("BPM is UInt16")
            return -1
        }
    } // END func deriveBeatsPerMinute


    func readSensorLocation(using sensorLocationCharacteristic: CBCharacteristic) -> String {

        let sensorLocationValue = sensorLocationCharacteristic.value!
        //  转换为无符号 8 位整数数组
        let buffer = [UInt8](sensorLocationValue)
        var sensorLocation = ""

        // 只看 8 位
        if buffer[0] == 1
        {
            sensorLocation = "Chest"
        }
        else if buffer[0] == 2
        {
            sensorLocation = "Wrist"
        }
        else
        {
            sensorLocation = "N/A"
        }

        return sensorLocation
    } // END func readSensorLocation


    func decodePeripheralState(peripheralState: CBPeripheralState) {

        switch peripheralState {
            case .disconnected:
                print("Peripheral state: disconnected")
            case .connected:
                print("Peripheral state: connected")
            case .connecting:
                print("Peripheral state: connecting")
            case .disconnecting:
                print("Peripheral state: disconnecting")
        }
    } // END func decodePeripheralState(peripheralState
} // END class HeartRateMonitorViewController

Refer

Core Bluetooth

Core Bluetooth Programming Guide