iOS Core Bluetooth 基础知识

原文:The Utimate Guide to Apple’s Core Bluetooth – PunchThrough

本文假设您了解蓝牙低能耗(BLE)和iOS编程的基础知识(包括许多iOS原生API常见的异步调用的委托模式),并旨在作为iOS核心蓝牙库来龙去向的综合指南。我们将指导您了解API的主要组件,包括扫描、连接和与BLE外围设备交互的基本步骤,以及iOS上BLE的常见陷阱和要了解的事情。

应用权限

在深入研究代码编写之前,您需要配置某些权限,以允许您的应用程序使用蓝牙。截至撰写本文时,Apple要求开发人员根据蓝牙使用情况,在应用程序的Info.plist中配置不同的蓝牙权限 Key-Value:

**Key:Privacy – Bluetooth Always Usage Description
**Value:**应用程序使用蓝牙的原因描述。
任何针对iOS 13或更高版本的应用程序都需要。

该应用程序首次启动时,将向用户提供描述,提示他们允许您的应用程序访问蓝牙。您应该描述清晰且诚实,例如,“此应用程序使用蓝牙查找和维护与[专有设备]的连接。”如果运行iOS 13或更高版本,未填写这个键值对将导致您的应用程序在启动时崩溃,并且您的应用程序将被App Store拒绝。

**Key:Privacy – Bluetooth Peripheral Usage Description
**Value:**面向用户的应用程序使用蓝牙的原因描述。
任何使用蓝牙并至少针对iOS 12或更低版本的应用程序都需要。

与上述规则相同。运行iOS 12或更低版本的设备将查找此键值,并向用户显示提供的消息,而运行iOS 13或更高版本的设备将使用上面列出的第一个键值对。项目 target 版本为12个或更低版本的应用程序应在Info.plist中提供这两个密钥。

**Key:Required background modes
**Value:**包含“应用程序使用Core Bluetooth通信”的数组
如果您想在后在使用蓝牙,您应该申请对应的后台蓝牙模式,包括扫描或者只是保持连接。

如下图:

10968377-00687d4fac9a7e9f

初始化中心Manager(CBCentralManager)

中心manager是设置蓝牙连接所需的第一个实例化对象。它能够进行蓝牙状态监控、扫描蓝牙外围设备、连接和断开蓝牙。

初始化CBCentralManager时,您设定CBCentralManagerDelegate协议的异步方法调用的代理,连接队列。在实践中,最好为蓝牙指定单独的队列,但这超出了本文的范围,因此我们将在代码示例中让队列默认为主队:

class BluetoothViewController: UIViewController {
    private var centralManager: CBCentralManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }
}

在本代码示例中,为了简单起见,委托设置为self,即存储中心管理器对象的同一类。

监控中心Manager的状态

仅仅实例化CBCentralManager对象不足以开始使用它。事实上,如果您尝试在初始化代码后立即调用scanForPeripherals(withServices:options:)您可能会在Xcode调试器中看到警告。您的CBCentralManagerDelegate必须实现centralManagerDidUpdateState()方法,并且您可以从那里继续您的流程。

每当更新手机和应用程序内的蓝牙状态时,Core Bluetooth都会调用centralManagerDidUpdateState()方法。在正常情况下,您应该在初始化中心Manager后几乎立即收到对委托对象的didUpdateState()调用,其中包含的状态为.poweredOn

从iOS 10中,可能的状态包括以下内容:

状态描述
poweredOn蓝牙已启用、授权并可供应用程序使用
poweredOff用户已关闭蓝牙,需要从“设置”或“控制中心”重新打开蓝牙
resetting与蓝牙服务的连接中断
unauthorized用户拒绝了应用程序使用蓝牙的权限。用户必须从应用程序的“设置”菜单中重新启用它
unsupportediOS 设备不支持蓝牙
unkown应用程序与蓝牙服务的连接未知

iOS有内置提示,似乎会通知用户应用程序需要蓝牙并请求访问,与iOS的大多数系统级提示和权限设置一样,该应用程序基本上没有控制。如果用户拒绝您的应用程序蓝牙访问,您将收到.unauthorized的CBState状态,在这种情况下,由您在程序中通过页面或者提示,让用户在系统设置中开启允许蓝牙访问状态。您甚至可以DeepLink来直接打开应用程序的设置页面。

对于禁用蓝牙的用户也是如此指导。不幸的是,在本文撰写时,苹果不提供API用DeepLink跳转到非应用程序特定的设置页面,如蓝牙。

⚠️您应该假设苹果提示的行为、用户界面和消息可能在iOS版本之间保持一致,并避免在自己的指令中过于具体地引用它们或试图预测它们的行为。

extension BluetoothViewController: CBCentralManagerDelegate {
 
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
            case .poweredOn:
                startScan()
            case .poweredOff:
                // Alert user to turn on Bluetooth 提示用户打开蓝牙
            case .resetting:
                // Wait for next state update and consider logging interruption of Bluetooth service
                // 等待下一个状态更新,并考虑记录蓝牙服务的中断
            case .unauthorized:
                // Alert user to enable Bluetooth permission in app Settings
                // 提示用户在设置中允许蓝牙权限
            case .unsupported:
                // Alert user their device does not support Bluetooth and app will not work as expected
                // 提示用户他们的设备不支持蓝牙,或者应用没有如期工作
            case .unknown:
               // Wait for next state update 等待下一个状态
               
        }
    }
}

扫描外围设备

一旦你在代理didUpdateStates()中获取到poweredOn状态,你就可以开始扫描蓝牙了。可以调用中心Manager的scanForPeripherals(withServices:options:)方法,你可能会收到一个 CBUUIDS(下面会介绍) 的你过滤到的服务数组。中心管理将通过代理方法centralManager(_:didDiscover:advertisementData:rssi:)返回至少含有一个服务的设备的广播。你可以在扫描方法中设置一系列可选项,通过使用字典的方式,字段包含下面的:

CBCentralManagerScanOptionAllowDuplicatesKey

此字段值类型为布尔,如果为真,则对给定设备的每个检测到的广播包进行委托调用,而不仅仅是扫描会话的第一个收到的广播包。默认值(在撰写此帖子时)为false,如果可能,Apple建议保留此设置,因为它使用的功率和内存比替代品少得多。然而,我们经常发现有必要打开此设置,以便在整个扫描会话中接收更新的RSSI值。

CBCentralManagerScanOptionSolicitedServiceUUIDKey

不常用,但在GAP外围广播到GAP中心时非常有用,在GAP中心设备充当GATT服务器而不是客户端(通常相反)。外围设备可以为其期望在中心关贸总协定表中看到的特定服务做广播(请求)。反过来,中心可以使用CBCentralManagerScanOptionSolicitedServiceUUIDKey在其扫描中包含为特定服务征求外围设备的外围设备。

*外围设备通常不会为其包含的所有甚至大多数服务做广播;相反,设备通常会为中心仅对特定类型的BLE设备感兴趣时应该知道的特定定制服务做广播。

外设标识符

与Android不同,出于安全考虑,iOS模糊了应用程序开发人员外围对象的MAC地址。相反,外围设备被分配一个随机生成的UUID,该UID位于CBPeripheral对象的标识符属性中。此UUID不能保证在扫描会话中保持不变,也不应100%依赖其进行外围设备重新识别。尽管如此,我们观察到,假设没有发生重大设备设置重置,从长远来看,它相对稳定和可靠。只要有替代方案,我们就可以在设备看不见时依赖它来提出连接请求等事情。

扫描结果

每次调用委托方法centralManager(_:didDiscover:advertisementData:rssi:)都会反映范围内BLE外围设备的检测到广播包。如上所述,给定扫描会话中每台设备的呼叫次数取决于提供的扫描选项,以及外围设备本身的范围和广播状态。

方法签名如下:

optional func centralManager(_ central: CBCentralManager, 
                didDiscover peripheral: CBPeripheral, 
                    advertisementData: [String : Any], 
                            rssi RSSI: NSNumber)

让我们分解上述方法的参数:

central:CBCentralManager

在扫描时发现设备的中心Manager对象。

peripheral:CBPeriphral

代表发现的BLE外围设备的CBPeripheral对象。我们将稍后部分更详细地介绍这种类型。

adverisementData:[String:Any]

检测到的广播包中包含的数据的字典表示。Core Bluetooth使用一组内置keys为我们分析和组织这些数据做得很好。

广播key名称和相关值类型
CBAdvertisementDataManufacturerDataKey NSData 外围制造商提供的自定义数据。外围设备可用于许多事情,例如存储设备序列号或其他识别信息。
CBAdvertisementDataServiceDataKey *[CBUUID:NSData]* 带有代表服务的CBUUID密钥以及与这些服务关联的自定义数据的词典。这通常是外围设备存储自定义识别数据以供预连接使用的最佳场所。
CBAdvertisementDataServiceUUIDsKey *[BUUID]* 一系列服务UUID,通常反映设备关贸总协定表中包含的一个或多个服务。
CBAdvertisementDataOverflowServiceUUIDsKey *[BUUID]* 来自广播数据溢出区域(扫描响应包)的服务UUID数组。适用于不适合主广播包的广播服务。
CBAdvertisementDataTxPowerLevelKey NSNumber 如果在广播包中提供外围设备的传输功率水平。
CBAdvertisementDataIsConnectable NSNumber NSNumber(0或1)中的布尔值,如果外围设备当前可连接,则为1。
CBAdvertisementDataSolicitedServiceUUIDsKey [BUUID] 一系列请求的服务UUID。请参阅CBCentralManagerScanOptionSolicitedServiceUIDKey部分中关于所请求服务的讨论。

rssi:NSNumber

接收广播包时外围设备的相对信号质量。由于RSSI是一个相对度量,中心解释的值可能因芯片组而异。正如大多数iOS设备通过Core Bluetooth返回的那样,它通常在-30到-99之间,其中-30是最强的。

// In main class
var discoveredPeripherals = [CBPeripheral]()
func startScan() {
    centralManager.scanForPeripherals(withServices: nil, options: nil)
}
…
…
…
// In CBCentralManagerDelegate class/extension
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    self.discoveredPeripherals.append(peripheral)
}

在上述示例中,我们只是将发现的外围设备存储在内部数组中,但这样做会丢失委托方法调用中返回的RSSI和广播数据。如果您以后想访问CBPeripheral对象,为CBPeripheral对象创建一个包装类或结构通常非常有用,其中包括这些项目的存储,因为CBPeripheral本身不支持这一点。

连接和断开连接

连接到外围设备

一旦您获得了对所需CBPeripheral对象,您只需在中心Manager上调用connect(_:options:)方法并在外围设备中传递即可尝试连接到它。您还可以在这里阅读一些连接选项,但我们不会在本帖中讨论它们。

当连接成功的时候,你会在centralManager(_:didConnect:)代理方法中收到。当然,连接失败的时候也会在代理方法centralManager(_:didFailToConnect:error:)中返回,这个方法包含了外设和错误。

您可以为超出范围的特定外围对象调用connect。如果您这样做,您将建立“连接请求”,iOS将无限期等待(除非蓝牙中断或应用程序被用户手动杀死),直到它看到设备进行连接并调用dedConnect委托方法。

// In main class
var connectedPeripheral: CBPeripheral?

func connect(peripheral: CBPeripheral) {
    centralManager.connect(peripheral, options: nil)
 }
…
…
… 
// In CBCentralManagerDelegate class/extension
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    // Successfully connected. Store reference to peripheral if not already done.
    self.connectedPeripheral = peripheral
}
 
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
    // Handle error
}

⚠️扫描回调返回CBPeripheral对象后,您必须在代码中保留对它的强引用。如果您只需立即从 doDiscover 委托方法调用 Connect,并让该函数块完成,而无需将外围设备强力存储到其他地方,则外围设备对象将被分配,任何连接或挂起连接都将中断。中心Manager不会在内部保留与其连接的外围设备的强大连接。

⚠️请注意,在iOS中,在建立基本连接后,在尝试任何配对或键合之前,会立即调用deConnect委托方法。令许多iOS开发人员失望的是,除了您可以通过加密服务和特性推断和触发的内容外,Core Bluetooth没有提供真正的公共API级对外围设备的绑定过程的洞察力或控制,我们将在本帖后面讨论。

⚠️在Core Bluetooth中,要球CBPeripheral对象的处于.connected状态,它必须在iOS BLE级别和应用程序级别连接。外围设备可能通过其他应用程序连接到iOS设备,或者因为它包含像HID这样的配置文件,可以触发自动重新连接。但是,您仍然需要从应用程序获得对外围设备的引用并调用connect()才能与它交互。有关更多信息,请参阅下面的粘合/配对讨论。

识别和引用CBPeripherals

苹果在Core Bluetooth中做出的另一个安全驱动的选择是模糊BLE外围设备的唯一MAC地址,该选择使其与Android蓝牙API(无论好坏)不同。除非被外围设备的固件隐藏在其他地方,例如在自定义广播数据、设备名称或特征中,否则根本无法从Core Bluetooth访问它。相反,Apple分配了一个唯一的UUID,该UUID在应用程序的上下文之外毫无意义,但可用于扫描和启动与该特定设备的连接(请参阅后台处理部分)。苹果明确表示,不能保证此UUID保持不变,也不应是识别外围设备的唯一方法。考虑到这一点,我们从我们的经验中发现,除了用户重置网络或其他出厂设置外,Apple分配的UID似乎在长期内仍然相当可靠。

在广播阶段识别外围设备的其他选项是按名称或自定义广播服务数据。如上所述,广播数据可以包括用于识别特定品牌的自定义服务UUID,甚至可以包括与广播包中这些服务链接的自定义数据,以进一步识别特定设备或设备集。

断开与外围设备的连接

要断开连接,只需调用cancelPeripheralConnection(_:)或删除对外围设备的所有强引用,即可隐式调用取消方法。您应该收到centralManagercentralManager(_:didDisconnectPeripheral:error:)委托调用以响应:

// In main class
func disconnect(peripheral: CBPeripheral) {
    centralManager.cancelPeripheralConnection(peripheral)
}
…
…
…
// In CBCentralManagerDelegate class/extension
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
    if let error = error {
        // Handle error
        return
    }
    // Successfully disconnected
}
iOS 发起的断开连接

iOS可能会在与外围设备有一段时间没有通信后断开连接(据说为30秒,但行为不能保证)。这通常由外围设备处理,带有某种甚至可能在iOS应用程序层看不到的心跳。

发现服务和特征

一旦您成功连接到外围设备,您可能会发现其服务,然后发现其特性。在这个过程中的这个时候,我们从使用CBCentralManager和CBCentralManagerDelegate方法转向CBPeripheral和CBPeripheralDelegate代理方法。此时,您需要分配外围对象的委托属性,以便接收这些委托回调:

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    self.connectedPeripheral = peripheral
    peripheral.delegate = self
}

(同样,为了简单起见,我们正在使用单个类来处理所有委托调用,但这不是大型代码库的最佳设计实践。)

发现服务

当你第一次发现并连接到CBPeripheral对象时,你会注意到它有一个services属性(类型为[CBService]?)。现在,它是nil。你需要通过简单的调用discoverServices([CBUUID]?)来发现外设的服务。您可以选择传递一组服务uuid,这将限制所发现的服务。这对于包含大量应用程序不关心的服务的外围设备来说很方便,因为忽略这些服务会更有效率(特别是在时间方面)。

还有类似的方法,discoverIncludedServices([CBUUID]?, for: CBService)服务可能表示它“包含”的其他相关服务,这意味着除了外围固件希望表示它们以某种方式相关外,没有什么意义。第二个参数应该是已经发现的服务,第一个参数允许您选择按照上一段所述筛选返回的服务。我们Punch Through公司没有发现对BLE的这一功能有什么用处,但如果有一个大的协定表,您可能想按组和/或不明确列出所有感兴趣的服务,它可能会有用。当然,这需要外围方的合作,以表明包含的服务。

发现服务后,您将收到CBPeripheral代理方法调用peripheral(_:didDiscoverServices:)其中CBService对象数组表示从传递到发现方法的可选提供的数组中发现的服务UUID(如果数组为零/空,将返回所有服务)。您还可以尝试打开CBPeripheral的服务属性,并注意到它现在包含迄今为止发现的服务数组。

发现特征

特征按服务分组。一旦您发现了外围设备的服务,您就可以发现每一个服务的特征。类似于CBPeripheral的services属性,你会注意到CBService有一个characteristics属性(类型为[CBCharacteristic]?)。一开始,它是nil

对于每个服务,在CBPeripheral对象上调用discoverCharacteristics([CBUUID?], for: CBService)可选地指定特定特征UUID,就像您对服务所做的那样。您应该会收到对您进行探索性调用的每个服务的peripheral(_:didDiscoverCharacteristicsFor:error:)的调用。

根据您的需求,您可能会发现在发现时存储您感兴趣的特征的引用非常有用,以避免在每个服务特征属性中现在填充的数组。每当您收到某些CBPeripheralDelegate回调时,这也将更容易快速识别引用的特征:

// In main class 
// Call after connecting to peripheral 在连接外设后调用
func discoverServices(peripheral: CBPeripheral) {
    peripheral.discoverServices(nil)
}
 
// Call after discovering services 在发现服务后调用
func discoverCharacteristics(peripheral: CBPeripheral) {
    guard let services = peripheral.services else {
        return
    }
    for service in services {
        peripheral.discoverCharacteristics(nil, for: service)
    }
}
…
…
…
// In CBPeripheralDelegate class/extension
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    guard let services = peripheral.services else {
        return
    }
    discoverCharacteristics(peripheral: peripheral)
}
 
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    guard let characteristics = service.characteristics else {
        return
    }
    // Consider storing important characteristics internally for easy access and equivalency checks later.
    // 考虑强引用特征属性,以便后面使用和检查
    // From here, can read/write to characteristics or subscribe to notifications as desired.
    // 至此,你可以读写特征或者订阅通知
}
描述符

在蓝牙中,特征描述符可以选择与某些特征一起提供,以提供有关其值的更多信息。例如,这些可以是人类可读的描述字符串,也可以是解释值的预期数据格式。在Core Bluetooth中,这些由CBDescriptor对象表示。它们的功能与特征相似,因为它们必须先被发现,然后才能被读取。

对于给定的特性,只需在目标外围设备上调用discoverDescriptors(for characteristic: CBCharacteristic)然后等待异步回调peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?)此时,CBCharacteristic对象的descriptors属性应为非零,并包含一组CBDescriptor对象。

CBDescriptors的不同可能类型由其uuid属性(CBUUID类型)区分开来,这些属性在Core Bluetooth文档中预定义。

CBDescriptor的value属性值在使用readValue(for descriptor: CBDescriptor)writeValue(_ data: Data, for descriptor: CBDescriptor)方法显式读取或写入之前都是nil。相关回调方法

peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) 

peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?)

相关代码

// In main class
func discoverDescriptors(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
    peripheral.discoverDescriptors(for: characteristic)  // 去发现特征
}
…
…
… 
// In CBPeripheralDelegate class/extension
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
    guard let descriptors = characteristic.descriptors else { return }
 
    // Get user description descriptor  获取到用户描述器
    if let userDescriptionDescriptor = descriptors.first(where: {
        return $0.uuid.uuidString == CBUUIDCharacteristicUserDescriptionString
    }) {
        // Read user description for characteristic 读取用户描述器的特征值
        peripheral.readValue(for: userDescriptionDescriptor)
    }
}
 
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
    // Get and print user description for a given characteristic
    // 获取和打印 用户描述中赋予的特征值
    if descriptor.uuid.uuidString == CBUUIDCharacteristicUserDescriptionString,
        let userDescription = descriptor.value as? String {
        print("Characterstic \(descriptor.characteristic.uuid.uuidString) is also known as \(userDescription)")
    }
}

⚠️在Core Bluetooth中,您不得编写客户端特征配置描述符(CBUUIDClientCharacteristicConfigurationString)的值,ß该描述符在iOS(以及Android BLE应用程序开发中明确使用)在表面下用于订阅或取消订阅有关该特征的通知/指示。相反,请使用setNotifyValue(_:for:)方法,如下节所述。

订阅通知和指示

如果一个特征支持通知(参见特征属性部分),你可以通过简单地调用setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic).来订阅通知/指示。通知和指示,虽然在BLE堆栈级别上功能不同,但在核心蓝牙中没有区别。如果订阅了某个特征,当该特征的值发生变化时,会调用该特征代理方法peripheral(_:didUpdateValueFor:error:),并从外围设备发送通知或指示。要取消订阅,只需调用setNotifyValue(false, for:characteristic)。每次你改变这个设置时,代理方法peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?)会被调用。

您可以通过检查特征的isNotifying属性来检查通知订阅状态:

// In main class
func subscribeToNotifications(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
    peripheral.setNotifyValue(true, for: characteristic)
 }
…
…
…
// In CBPeripheralDelegate class/extension
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
    if let error = error {
        // Handle error 处理错误
        return
    }
    // Successfully subscribed to or unsubscribed from notifications/indications on a characteristic
    // 成功订阅/取消订阅 特征回调
}

从特征中读出值

如果该特征具备’read’能力,您可以通过在CBPeripheral对象上调用readValue(for:CBCharacteristic)来读取其值。该值通过CBPeripheralDelegate的peripheral(_:didUpdateValueFor:error:)方法返回:

// In main class
func readValue(characteristic: CBCharacteristic) {
    self.connectedPeripheral?.readValue(for: characteristic)
}
… 
…
…
// In CBPeripheralDelegate class/extension
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    if let error = error {
        // Handle error
        return 
    }
    guard let value = characteristic.value else {
        return
    }
    // Do something with data 做一些数据处理
}

写一个特征

要写入特征,请在CBPierpheral对象上调用writeValue(_ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType))。这种方法的参数按顺序是要写入的数据、要写入的特征和写入类型。

有两种可能的写入类型:.withResponse.withoutResponse。它们分别对应于BLE中所谓的写入请求和写入命令。

对于写请求(. withresponse), BLE层将要求外设返回一个确认,即写请求已被接收并成功完成。在核心蓝牙层,当请求完成或出错时,你会收到一个对外围设备(_:didUpdateValueFor:error:)的调用。

对于写入命令(.withoutResponse),假设从iOS角度来看写入成功,在收到书面值后不会发送确认,也不会发生委托回调。也就是说,iOS能够在存在内部问题的情况下成功执行写入操作。

写入请求更健壮,因为您可以保证交付,或者是显式错误。它们通常更适合一次性注销操作。然而,如果您在多个连续的写入操作中发送大量批量数据,等待每个操作的确认可能会显著减慢整个过程的速度。相反,如果适用,请考虑在FW-mobile协议中构建一定级别的数据包跟踪。

// In main class
func write(value: Data, characteristic: CBCharacteristic) {
    self.connectedPeripheral?.writeValue(value, for: characteristic, type: .withResponse)
    // OR
   self.connectedPeripheral?.writeValue(value, for: characteristic, type: .withoutResponse)
 }
…
…
…
// In CBPeripheralDelegate class/extension
// Only called if write type was .withResponse  只有在响应模式下才会回调
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
    if let error = error {
        // Handle error
        return
    }
    // Successfully wrote value to characteristic
}
最大化背靠背写命令:那得多块呀?

虽然写入命令(没有响应)的性质是无法保证另一方的数据包交付,但您仍然希望确保以合理的速度发送响应,不会超过iOS的内部专用队列缓冲区。在iOS 11之前,这只是猜测,但此后,他们增加了CBPeripheral和CBPeripheralDelegate API,以缓解这种情况:

CBPeripheral property canSendWriteWithoutResponse: Bool 

CBPeripheralDelegate method peripheralIsReady(toSendWriteWithoutResponse: CBPeripheral)

在发送写入命令之前,您应该始终检查canSendWriteWithoutResponse,如果是false,则等待对 peripheralIsReady(toSendWriteWithoutResponse:)的调用,然后再继续。请注意,在canSendWriteWithoutResponse设置为true和实际调用委托方法的情况下,我们观察到了不同程度的可靠性,特别是在恢复的外围设备的情况下,因此您可能不想仅依赖此API允许写入命令发生。您还可以在计时器上实现没有响应的写入,尽管您需要根据数据大小在保守方面犯错误。

// In main class
func write(value: Data, characteristic: CBCharacteristic) {
    if connectedPeripheral?.canSendWriteWithoutResponse {
        self.connectedPeripheral?.writeValue(value, for: characteristic, type: .withoutResponse)
    }
}
…
…
…
// In CBPeripheralDelegate class/extension
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
    // Called when peripheral is ready to send write without response again.
    // 外设准备好了再次发送写(以没有响应方式)时调用
    // Write some value to some target characteristic.
    // 写值到目标特征中
    write(value: someValue, characteristic: someCharacteristic)
}
最大写入长度

从iOS 9起,Core Bluetooth提供了一种方便的方法来确定给定特征的最大字节长度,对于有响应的写入和没有响应的写入,这可能有所不同:

maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int

您的通信可以支持的实际最大写入长度取决于中心和外围设备的相应BLE堆栈。此方法只需返回iOS设备将支持该操作的最大写入长度。尝试在一方或双方超过已知限制的写入时的行为尚未定义。

配对和粘合

从iOS 9起,未经绑定(配对后为未来的连接额外安全交换和存储长期keys),就不允许配对(为一次性安全连接交换临时key)。

根据外围设备的BLE安全设置的配置方式,可以在连接点或尝试读取、写入或订阅加密特征来触发配对/粘合过程。苹果的配件设计指南实际上鼓励BLE设备制造商使用不充分的身份验证方法(即从加密特征中读取)来触发粘结,但我们的研究表明,虽然这对Android设备来说通常也很成功,但最可靠的方法往往因制造商和型号而异。

同样,根据外围设备的配置,配对过程中的用户界面流程将如下:

  • 用户会收到警告,提示他们输入PIN。用户必须输入正确的PIN才能配对/绑定才能继续。
  • 用户会看到一个简单的是/否对话框,提示他们允许继续配对。
  • 用户没有对话框,配对/粘合在表面下完成。

令许多iOS开发人员非常懊恼的是,Core Bluetooth没有深入了解配对和绑定过程,也没有提供外围设备的配对/绑定状态。API仅通知开发人员设备是否已连接。

在某些情况下,例如对于HID设备,一旦外围设备被粘合,iOS将在看到外围设备广播时自动连接到它。这种行为独立于任何应用程序发生,外围设备可以连接到iOS设备,但不能连接到最初建立债券的应用程序。如果绑定的外围设备断开与iOS设备的连接,然后在iOS级别重新连接,应用程序将需要检索外围设备对象(retrieveConnectedPeripherals(with[Services/Identifiers]:),并通过CBCentralManager再次显式连接,以建立应用程序级连接。要使用此方法检索您的设备,您必须从之前返回的CBPeripheral对象或其中包含的至少一项服务中指定Apple分配的设备标识符。

⚠️ iOS不允许开发人员从应用缓存中清除外围设备的绑定状态。要清除绑定,用户必须前往iOS设置的蓝牙部分,并显式“忘记”外围设备。如果会影响用户体验,将这些信息包含在应用程序的用户界面中可能会有所帮助,因为大多数用户不会知道这一点。

Core蓝牙错误

CBCentralManagerDelegate和CBPeripheralDelegate协议中的几乎所有方法都包含Error?键入参数,如果发生错误,则为非nil。您可以期望这些错误是CBErrorCBATTError类型。除此之外,苹果没有明确哪些方法可能会具体返回哪些错误,甚至可能是哪种类型,因此围绕单个Core蓝牙错误的所有可能行为仍然有些未知。

通常,当ATT层出现问题时,会返回CBATTError。这包括对加密特性的访问问题、不受支持的操作(例如,对只读特性的写入操作)以及一系列其他错误,这些错误通常仅在您使用CBPeripheralManager API将iOS设备设置为外围设备时才适用,而外围设备通常是ATT服务器在大多数蓝牙设备上居住的地方。

谢谢你的阅读!

无论您是经验丰富的BLE开发人员还是刚刚开始,我们希望您发现这篇文章有用且信息量很大。虽然我们只从中心角色的角度介绍了核心蓝牙的基础知识,但还有很多东西要讨论!敬请关注未来在iOS上Core Bluetooth和BLE上的帖子,同时查看我们关于iOS开发的其他一些帖子:

如何处理iOS 13的新蓝牙权限
利用后台蓝牙获得出色的用户体验

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值