iOS Core Bluetooth Basics

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

This article assumes an understanding of the basics of Bluetooth Low Energy (BLE) and iOS programming (including the delegate pattern for asynchronous calls common to many iOS native APIs), and is intended to serve as a comprehensive guide to the ins and outs of the iOS core Bluetooth library. We'll guide you through the main components of the API, including the basic steps to scan, connect, and interact with BLE peripherals, as well as common pitfalls and things to know about BLE on iOS.

app permissions

Before diving into coding, you need to configure certain permissions to allow your application to use Bluetooth. As of this writing, Apple requires developers to configure different Bluetooth permission Key-Values ​​in the app's Info.plist depending on the Bluetooth usage:

**Key: Privacy – Bluetooth Always Usage Description
**Value: ** A description of why the application uses Bluetooth.
Required for any app targeting iOS 13 or later.

When the app first launches, the user will be provided with a description, prompting them to allow your app to access Bluetooth. You should describe it clearly and honestly, for example, "This app uses Bluetooth to find and maintain a connection to [proprietary device]." If running iOS 13 or later, failing to fill in this key-value pair will cause your app to appear in the Crash on launch and your app will be rejected by the App Store.

**Key: Privacy – Bluetooth Peripheral Usage Description
**Value: ** A description of why the user-facing application uses Bluetooth.
Required for any app that uses bluetooth and targets at least iOS 12 or lower.

Same rules as above. Devices running iOS 12 or lower will look for this key-value and display the provided message to the user, while devices running iOS 13 or higher will use the first key-value pair listed above. Apps with a project target version 12 or lower should provide these two keys in the Info.plist.

**Key: Required background modes
**Value:** Contains an array of "applications using Core Bluetooth communication"
. If you want to use Bluetooth later, you should apply for the corresponding background Bluetooth mode, including scanning or just keeping connected.

As shown below:

10968377-00687d4fac9a7e9f

Initialize the central Manager (CBCentralManager)

The central manager is the first instantiated object required to set up a Bluetooth connection. It is capable of Bluetooth status monitoring, scanning for Bluetooth peripherals, connecting and disconnecting from Bluetooth.

When initializing the CBCentralManager, you set up CBCentralManagerDelegatethe proxy for the protocol's asynchronous method calls, the connection queue. In practice, it's better to specify a separate queue for bluetooth, but that's beyond the scope of this article, so we'll let the queue default to the main queue in the code example:

class BluetoothViewController: UIViewController {
    
    
    private var centralManager: CBCentralManager!

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

In this code example, for simplicity, the delegate is set to self, the same class of the Storage Center Manager object.

The status of the monitoring center Manager

Just instantiating a CBCentralManager object is not enough to start using it. scanForPeripherals(withServices:options:)In fact, you may see warnings in the Xcode debugger if you try to call immediately after initialization code . Your CBCentralManagerDelegate must implement centralManagerDidUpdateState()method and you can continue your process from there.

Method called by Core Bluetooth whenever the Bluetooth state is updated on the phone and within the app centralManagerDidUpdateState(). Under normal circumstances, you should receive didUpdateState()a call to the delegate object almost immediately after initializing the central Manager, which contains the state .poweredOn.

From iOS 10, possible states include the following:

state describe
poweredOn Bluetooth is enabled, authorized and available to the application
poweredOff The user has turned off Bluetooth and needs to turn it back on from Settings or Control Center
resetting Lost connection to bluetooth service
unauthorized The user denied the application permission to use Bluetooth. Users must re-enable it from the app's Settings menu
unsupported iOS devices do not support Bluetooth
unknown The app's connection to the bluetooth service is unknown

iOS has built-in prompts that seem to inform the user that an app requires Bluetooth and requests access, and like most of iOS' system-level prompts and permission settings, the app has essentially no control. If the user rejects your application's Bluetooth access, you will receive .unauthorizedthe CBState status. In this case, you can ask the user to enable the Bluetooth access status in the system settings through the page or prompt in the program. You can even DeepLink to directly open the app's settings page.

The same guidance applies to users who have disabled Bluetooth. Unfortunately, at the time of writing, Apple does not provide an API to jump to non-app-specific settings pages with DeepLink, such as Bluetooth.

⚠️You should assume that the behavior of Apple prompts, UI, and messages is likely to be consistent across iOS versions, and avoid referencing them too specifically or trying to predict their behavior in your own instructions.

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 等待下一个状态
               
        }
    }
}

Scan for peripherals

Once you didUpdateStates()get poweredOnthe state in the proxy, you can start scanning for bluetooth. You can call the method of the central Manager scanForPeripherals(withServices:options:), and you may receive a CBUUIDS (described below) array of services you filtered. Central management will centralManager(_:didDiscover:advertisementData:rssi:)return broadcasts for devices with at least one service via proxy methods. You can set a series of optional items in the scan method, by using the dictionary, the field contains the following:

CBCentralManagerScanOptionAllowDuplicatesKey

This field value type is boolean, if true, the delegate call is made for every detected advertisement packet for the given device, not just the first received advertisement packet for the scan session. The default (at the time of writing this post) is false, and Apple recommends keeping this setting if possible, as it uses much less power and memory than alternatives. However, we often find it necessary to turn this setting on in order to receive updated RSSI values ​​throughout a scan session.

CBCentralManagerScanOptionSolicitedServiceUUIDKey

Not commonly used, but useful when the GAP Peripheral broadcasts to the GAP Central, where the device acts as a GATT server rather than a client (usually the opposite). Peripherals can make broadcasts (requests) for specific services they expect to see in the central GATT table. In turn, the central can use the CBCentralManagerScanOptionSolicitedServiceUUIDKey to include in its scans peripherals that solicit peripherals for a particular service.

*Peripherals typically do not advertise all or even most of the services they contain; instead, devices typically advertise specific custom services that the hub should only know about if it is interested in a specific type of BLE device.

peripheral identifier

Unlike Android, iOS obfuscates the MAC addresses of peripheral objects from app developers for security reasons. Instead, peripherals are assigned a randomly generated UUID, which is located in the identifier property of the CBPeripheral object. This UUID is not guaranteed to persist across scan sessions, and should not be relied upon 100% for peripheral re-identification. Nonetheless, we have observed that, assuming no major device settings resets occur, it is relatively stable and reliable in the long run. As long as there is an alternative, we can rely on it to make things like connection requests when the device is out of sight.

scan result

Each call to the delegate method centralManager(_:didDiscover:advertisementData:rssi:)will reflect the detected broadcast packets of the BLE peripherals within range. As mentioned above, the number of calls per device in a given scan session depends on the scan options provided, as well as the range and advertising status of the peripheral itself.

The method signature is as follows:

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

Let's break down the parameters of the above method:

central:CBCentralManager

The central Manager object that discovers devices while scanning.

peripheral:CBPeriphral

A CBPeripheral object representing the discovered BLE peripheral. We'll cover this type in more detail in a later section.

adverisementData:[String:Any]

A dictionary representation of the data contained in detected broadcast packets. Core Bluetooth does a good job analyzing and organizing this data for us using a set of built-in keys .

Broadcast key names and associated value types
CBAdvertisementDataManufacturerDataKey NSData Custom data provided by the peripheral manufacturer. Peripherals can be used for many things, such as storing a device serial number or other identifying information.
CBAdvertisementDataServiceDataKey * [CBUUID:NSData]* Dictionary with CBUUID keys representing services and custom data associated with those services. This is usually the best place for a peripheral to store custom identification data for pre-connect use.
CBAdvertisementDataServiceUUIDsKey * [BUUID] * An array of service UUIDs, typically reflecting one or more services contained in the device's GATT table.
CBAdvertisementDataOverflowServiceUUIDsKey * [BUUID] * An array of service UUIDs from the AdvertisementDataOverflowServiceUUIDsKey (scan response packet). Applies to broadcast services that do not fit into the main broadcast package.
CBAdvertisementDataTxPowerLevelKey NSNumber If the transmit power level of the peripheral is provided in the advertisement packet.
CBAdvertisementDataIsConnectable NSNumber Boolean in NSNumber (0 or 1), 1 if the peripheral is currently connectable.
CBAdvertisementDataSolicitedServiceUUIDsKey [BUUID] An array of requested service UUIDs. See the discussion of the requested service in the CBCentralManagerScanOptionSolicitedServiceUIDKey section.

rssi:NSNumber

The relative signal quality of the peripheral when receiving broadcast packets. Since RSSI is a relative measure, the values ​​interpreted by the center may vary by chipset. As most iOS devices return via Core Bluetooth, it's usually between -30 and -99, with -30 being the strongest.

// 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)
}

In the above example we just store the discovered peripherals in an internal array, but doing so loses the RSSI and broadcast data returned in the delegate method call. It is often useful to create a wrapper class or struct for the CBPeripheral object that includes storage for these items if you later want to access the CBPeripheral object, since CBPeripheral does not natively support this.

connect and disconnect

Connect to peripherals

Once you have obtained a reference to the desired CBPeripheral object, you simply call a method on the central Manager connect(_:options:)and pass in the peripheral to attempt to connect to it. You can also read about some connection options here , but we won't be discussing them in this post.

centralManager(_:didConnect:)You will receive it in the delegate method when the connection is successful . Of course, when the connection fails, it will also centralManager(_:didFailToConnect:error:)return in the proxy method, which contains peripherals and errors.

You can call connect for a specific enclosing object that goes out of scope. If you do this, you will establish a "connection request" and iOS will wait indefinitely (unless bluetooth is interrupted or the app is manually killed by the user) until it sees the device making a connection and calls the dedConnect delegate method.

// 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
}

⚠️ After the scan callback returns the CBPeripheral object, you must keep a strong reference to it in your code. If you just call Connect immediately from the doDiscover delegate method and let that function block complete without brute force storing the peripheral elsewhere, the peripheral object will be allocated and any connections or pending connections will be aborted. The central Manager does not internally keep strong connections to the peripherals it connects to.

⚠️ Note that in iOS, the deConnect delegate method is called immediately after the basic connection is established, before any pairing or bonding is attempted. To the disappointment of many iOS developers, Core Bluetooth provides no real public API-level insight or control into the binding process to a peripheral, beyond what you can infer and trigger via cryptographic services and characteristics, which we will discuss in this Discuss later in the post.

⚠️In Core Bluetooth, for a CBPeripheral object to be .connectedstateful, it must be connected at both the iOS BLE level and the application level. The peripheral may connect to the iOS device through another app, or because it contains a profile like HID, can trigger an automatic reconnect. However, you still need to get a reference to the peripheral and call it from your application connect()in order to interact with it. See the bonding/pairing discussion below for more information.

Identify and reference CBPeripherals

Another security-driven choice Apple made in Core Bluetooth is to obfuscate the unique MAC address of a BLE peripheral, a choice that makes it different from the Android Bluetooth API (for better or worse). It simply cannot be accessed from Core Bluetooth unless it is hidden elsewhere by the peripheral's firmware, such as in custom advertisement data, device name, or characteristics. Instead, Apple assigns a unique UUID that is meaningless outside the context of the application, but can be used to scan for and initiate connections to that specific device (see the Background Processing section). Apple makes it clear that this UUID is not guaranteed to remain the same, nor should it be the only way to identify a peripheral. With that in mind, we've found from our experience that Apple-assigned UIDs seem to remain fairly reliable in the long-term, barring a user reset of the network or other factory settings.

Other options for identifying peripherals during the advertising phase are by name or custom advertising service data. As mentioned above, broadcast data can include custom service UUIDs to identify a particular brand, and can even include custom data linked to these services in the broadcast package to further identify a specific device or set of devices.

Disconnect from peripherals

To disconnect, simply call cancelPeripheralConnection(_:)or remove all strong references to the peripheral, which implicitly calls the cancel method. You should receive the centralManager centralManager(_:didDisconnectPeripheral:error:)delegate call in response:

// 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-initiated disconnect

iOS may disconnect after a period of no communication with the peripheral (say 30 seconds, but behavior is not guaranteed). This is usually handled by the peripheral, with some sort of heartbeat that may not even be visible at the iOS application layer.

Discovery Services and Characteristics

Once you have successfully connected to a peripheral, you may discover its services and then its characteristics. At this point in the process, we move from using the CBCentralManager and CBCentralManagerDelegate methods to the CBPeripheral and CBPeripheralDelegate delegate methods. At this point, you need to assign the enclosing object's delegate property to receive these delegate callbacks:

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

(Again, we're using a single class to handle all delegate calls for simplicity, but that's not a best design practice for large codebases.)

discovery service

When you first discover and connect to a CBPeripheral object, you'll notice that it has a servicesproperty (of type [CBService]?). Now, it is nil. discoverServices([CBUUID]?)You need to discover the peripheral's services with simple calls . You can optionally pass an array of service uuids, which will limit the services discovered. This is handy for peripherals that contain a lot of services that the application doesn't care about, since it would be more efficient (especially in terms of timing) to ignore them.

There's a similar approach where discoverIncludedServices([CBUUID]?, for: CBService)a service might represent other related services it "contains", meaning that it doesn't make much sense other than that the peripheral firmware wants to represent that they are related in some way. The second parameter should be the discovered service, the first parameter allows you to optionally filter the returned services as described in the previous paragraph. We at Punch Through haven't found much use for this feature over BLE, but it might be useful if you have a large protocol table where you might want to list all services of interest by group and/or not explicitly. Of course, this requires the cooperation of peripheral parties to indicate the included services.

Once a service is discovered, you will receive a CBPeripheral proxy method call where peripheral(_:didDiscoverServices:)the array of CBService objects represents the service UUIDs discovered from the optionally provided array passed to the discover method (if the array is zero/empty, all services will be returned). You can also try opening CBPeripheral's Services properties and notice that it now contains an array of services discovered so far.

Discovery features

Characteristics are grouped by service. Once you have discovered the peripheral's services, you can discover the characteristics of each service. Similar to the services property of CBPeripheral, you will notice that CBService has a characteristicsproperty (of type [CBCharacteristic]?). At first, it was nil.

For each service, call on the CBPeripheral object discoverCharacteristics([CBUUID?], for: CBService)optionally specifying a specific characteristic UUID, just like you do for services. You should receive a call to each service that you make exploratory calls to peripheral(_:didDiscoverCharacteristicsFor:error:).

Depending on your needs, you may find it useful to store references to the traits you are interested in at discovery time, to avoid the now populated array in each service trait property. This will also make it easier to quickly identify referenced traits whenever you receive certain CBPeripheralDelegate callbacks:

// 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.
    // 至此,你可以读写特征或者订阅通知
}

Descriptor

In Bluetooth, characteristic descriptors can optionally be provided with certain characteristics to provide more information about their value. For example, these could be human-readable description strings, or the expected data format to interpret the value. In Core Bluetooth, these are represented by CBDescriptor objects. They function like traits in that they must be discovered before they can be read.

For a given characteristic, simply call discoverDescriptors(for characteristic: CBCharacteristic)and then wait for an asynchronous callback on the target peripheral peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?). At this point, the CBCharacteristic object descriptorsshould have non-zero properties and contain an array of CBDescriptor objects.

The different possible types of CBDescriptors uuidare distinguished by their properties (CBUUID types), which are predefined in the Core Bluetooth documentation.

The value of the value attribute of CBDescriptor is until it is explicitly read or written using the readValue(for descriptor: CBDescriptor)or method . Related callback methodswriteValue(_ data: Data, for descriptor: CBDescriptor)nil

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

and

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

related code

// 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)")
    }
}

⚠️ In Core Bluetooth, you must not write the value of the client characteristic configuration descriptor ( CBUUIDClientCharacteristicConfigurationString ), ß this descriptor is used explicitly in iOS (and Android BLE app development) to subscribe or unsubscribe about the characteristic notifications/instructions. Instead, use setNotifyValue(_:for:)methods, as described in the next section.

Subscribe to notifications and instructions

setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic)If a trait supports notifications (see the trait properties section), you can subscribe to notifications/indications by simply calling . Notifications and indications, while functionally different at the BLE stack level, are not different in core Bluetooth. If subscribed to a characteristic, when the value of the characteristic changes, the characteristic delegate method is called peripheral(_:didUpdateValueFor:error:)and a notification or indication is sent from the peripheral. To unsubscribe, just call setNotifyValue(false, for:characteristic). Every time you change this setting, the delegate method peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?)will be called.

You can isNotifyingcheck notification subscription status by checking the properties of the characteristic:

// 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 value from feature

If the characteristic has 'read' capability, you can read its value readValue(for:CBCharacteristic)by . The value peripheral(_:didUpdateValueFor:error:)is returned by the method of CBPeripheralDelegate:

// 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 做一些数据处理
}

write a feature

To write to a characteristic, call it on a CBPierpheral object writeValue(_ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType)). The arguments to this method are, in that order, the data to write, the characteristics to write, and the type of write.

There are two possible write types: .withResponseand .withoutResponse. They correspond to the so-called Write Request and Write Command in BLE respectively.

For write requests (.withresponse), the BLE layer will ask the peripheral to return an acknowledgment that the write request was received and completed successfully. At the core bluetooth layer, you receive a (_:didUpdateValueFor:error:)call to the peripheral when a request completes or an error occurs.

For the write command ( .withoutResponse), assuming the write was successful from an iOS perspective, no acknowledgment is sent and no delegate callback occurs after the written value is received. That said, iOS is able to successfully perform write operations despite internal issues.

Write requests are more robust because you can guarantee delivery, or explicit errors. They are usually better suited for one-time logout operations. However, if you are sending large batches of data in multiple consecutive write operations, waiting for an acknowledgment for each operation can significantly slow down the entire process. Instead, consider building some level of packet tracing into the FW-mobile protocol, if applicable.

// 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
}

Maximize back-to-back write commands: How much is that?

While the nature of the write command (without a response) is such that there is no guarantee of packet delivery from the other side, you still want to ensure that responses are sent at a reasonable rate and don't exceed iOS's internal private queue buffers. Prior to iOS 11, this was just speculation, but since then, they've added the CBPeripheral and CBPeripheralDelegate APIs to ease this:

CBPeripheral property canSendWriteWithoutResponse: Bool 

and

CBPeripheralDelegate method peripheralIsReady(toSendWriteWithoutResponse: CBPeripheral)

You should always check before sending a write command canSendWriteWithoutResponse, and if false, wait peripheralIsReady(toSendWriteWithoutResponse:)for a call to before continuing. Note that canSendWriteWithoutResponsewe've observed varying degrees of reliability between setting this to true and actually calling the delegate method, especially in the case of recovered peripherals, so you probably don't want to rely solely on this API to allow write commands to happen. You can also implement unresponsive writes on timers, though you need to err on the side of conservatism depending on the size of the data.

// 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)
}

Maximum write length

As of iOS 9, Core Bluetooth provides a convenience method to determine the maximum byte length for a given characteristic, which may differ for writes with and without a response:

maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int

The actual maximum write length your communication can support depends on the respective BLE stacks of the central and peripheral devices. This method simply returns the maximum write length that the iOS device will support for the operation. The behavior when attempting to write beyond known limits on one or both parties is undefined.

pairing and bonding

As of iOS 9, pairing (exchanging temporary keys for a one-time secure connection) is not allowed without bonding (exchanging and storing long-term keys for additional security after pairing for future connections).

Depending on how the peripheral's BLE security settings are configured, the pairing/gluing process can be triggered at the point of connection or by attempting to read, write, or subscribe to an encrypted characteristic. Apple's accessory design guidelines actually encourage BLE device manufacturers to use insufficient authentication methods (i.e. reading from encrypted characteristics) to trigger bonding, but our research shows that while this is often successful for Android devices as well , but the most reliable method tends to vary by manufacturer and model.

Again, depending on the configuration of the peripheral, the UI flow during the pairing process will be as follows:

  • Users are alerted, prompting them to enter their PIN. User must enter correct PIN to pair/bond to proceed.
  • Users see a simple yes/no dialog prompting them to allow pairing to continue.
  • There are no dialogs for the user and pairing/gluing is done beneath the surface.

Much to the chagrin of many iOS developers, Core Bluetooth doesn't provide insight into the pairing and bonding process, nor does it provide the pairing/bonding status of a peripheral. The API only notifies the developer if the device is connected.

In some cases, such as for HID devices, once the peripheral is bonded, iOS will automatically connect to it when it sees the peripheral broadcasting. This behavior happens independently of any app, the peripheral can connect to the iOS device but not to the app that originally established the bond. If a bound peripheral disconnects from the iOS device and then reconnects at the iOS level, the application will need to retrieve the peripheral object ( retrieveConnectedPeripherals(with[Services/Identifiers]:) and explicitly connect again through the CBCentralManager to establish an application-level connection. To retrieve your device using this method, you must specify an Apple-assigned device identifier from the previously returned CBPeripheral object or from at least one of the services contained within it.

⚠️ iOS does not allow developers to clear the binding state of peripherals from the app cache. To clear the bond, the user must go to the Bluetooth section of iOS settings and explicitly "forget" the peripheral. If it affects the user experience, it may be helpful to include this information in the application's user interface, since most users will not be aware of it.

Core bluetooth error

Almost all methods in the CBCentralManagerDelegate and CBPeripheralDelegate protocols contain Error?typed parameters, which are not if an error occurs nil. You can expect these errors to be CBErroror CBATTErrortype. Beyond that, Apple hasn't specified which methods might return which errors specifically, or even what type they might be, so all possible behaviors surrounding a single Core Bluetooth error are still somewhat unknown.

Usually, when there is a problem with the ATT layer, CBATTError will be returned. This includes access issues with encrypted properties, unsupported operations (for example, writes to read-only properties), and a host of other errors that typically only occur when you use the CBPeripheralManager API to set up an iOS device as a peripheral Applicable, while Peripherals is usually where the ATT server lives on most Bluetooth devices.

Thank you for reading!

Whether you are an experienced BLE developer or just getting started, we hope you found this article useful and informative. While we've only covered the basics of core Bluetooth from the perspective of a central character, there's a lot more to discuss! Stay tuned for future posts on Core Bluetooth and BLE on iOS, and also check out some of our other posts on iOS development:

How to Handle iOS 13's New Bluetooth Permissions
Leverage Background Bluetooth for a Great User Experience

Guess you like

Origin blog.csdn.net/qq_14920635/article/details/122133494