mcpField
C
core-bluetooth

core-bluetooth

github.com/dpearson2699开发1.2k3.8k 次安装13 周前更新
代码仓库

关于 core-bluetooth

面向 Swift 开发者的 Core Bluetooth 技能,介绍 BLE 设备扫描、连接与 GATT 数据交换,也涵盖外围设备广播、后台运行和状态恢复。

主要功能

  1. 01扫描并连接 BLE 外围设备
  2. 02发现服务与特征
  3. 03读写数据并订阅通知
  4. 04作为外围设备广播服务
  5. 05配置后台 BLE 模式
  6. 06恢复蓝牙管理器状态

适用场景

  1. 01连接并读取 BLE 传感器数据
  2. 02向外围设备写入特征值
  3. 03让应用在后台扫描或广播
  4. 04在 BLE 事件后恢复应用状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-bluetooth

安装后在 Claude Code 中输入/core-bluetooth确认已加载。

附带文件evals/evals.jsonreferences/ble-patterns.md

详细介绍

Core Bluetooth

扫描、连接蓝牙低功耗(BLE)设备并与其交换数据。 涵盖中心角色(扫描并连接外围设备)、外围角色(广播服务)、后台模式和状态恢复。 对于保护隐私的配件发现和设置,请使用 accessorysetupkit; 对于直接的 Core Bluetooth GATT 通信,请使用此技能。

目录

设置

Info.plist 键
键用途
NSBluetoothAlwaysUsageDescription必需。说明应用使用蓝牙的原因
UIBackgroundModes with bluetooth-central后台扫描和连接
UIBackgroundModes with bluetooth-peripheral后台广播
蓝牙授权

Core Bluetooth 没有显式的权限请求 API。添加 NSBluetoothAlwaysUsageDescription,在应用准备好使用蓝牙时创建管理器,然后检查 manager.authorization 和 manager.state。 将 .denied 和 .restricted 视为终止状态,直到用户在“设置”中更改权限; 等待状态变为 .poweredOn 后,再扫描、连接、广播或发布服务。

中心角色:扫描

创建中心管理器

扫描前始终等待状态变为 poweredOn。

import CoreBluetooth

final class BluetoothManager: NSObject, CBCentralManagerDelegate {
    private var centralManager: CBCentralManager!
    private var discoveredPeripheral: CBPeripheral?

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

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        guard central.state == .poweredOn else { return }
        startScanning()
    }
}
扫描外围设备

扫描特定服务 UUID 以节省电量。传入 nil 可发现所有外围设备(不建议在生产环境中这样做)。

let heartRateServiceUUID = CBUUID(string: "180D")

func startScanning() {
    centralManager.scanForPeripherals(
        withServices: [heartRateServiceUUID],
        options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
    )
}

func centralManager(
    _ central: CBCentralManager,
    didDiscover peripheral: CBPeripheral,
    advertisementData: [String: Any],
    rssi RSSI: NSNumber
) {
    guard RSSI.intValue > -70 else { return } // Filter weak signals

    // IMPORTANT: Retain the peripheral -- it will be deallocated otherwise
    discoveredPeripheral = peripheral
    centralManager.stopScan()
    centralManager.connect(peripheral, options: nil)
}

中心角色:连接

func centralManager(
    _ central: CBCentralManager,
    didConnect peripheral: CBPeripheral
) {
    peripheral.delegate = self
    peripheral.discoverServices([heartRateServiceUUID])
}

func centralManager(
    _ central: CBCentralManager,
    didDisconnectPeripheral peripheral: CBPeripheral,
    timestamp: CFAbsoluteTime,
    isReconnecting: Bool,
    error: Error?
) {
    if isReconnecting {
        // System is automatically reconnecting
        return
    }
    // Handle disconnection -- optionally reconnect
    discoveredPeripheral = nil
}

发现服务和特征

实现 CBPeripheralDelegate,遍历服务/特征树。

extension BluetoothManager: CBPeripheralDelegate {
    func peripheral(
        _ peripheral: CBPeripheral,
        didDiscoverServices error: Error?
    ) {
        guard let services = peripheral.services else { return }
        for service in services {
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }

    func peripheral(
        _ peripheral: CBPeripheral,
        didDiscoverCharacteristicsFor service: CBService,
        error: Error?
    ) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            if characteristic.properties.contains(.notify) {
                peripheral.setNotifyValue(true, for: characteristic)
            }
            if characteristic.properties.contains(.read) {
                peripheral.readValue(for: characteristic)
            }
        }
    }
}

读取、写入和通知

读取值
func peripheral(
    _ peripheral: CBPeripheral,
    didUpdateValueFor characteristic: CBCharacteristic,
    error: Error?
) {
    guard let data = characteristic.value else { return }

    switch characteristic.uuid {
    case CBUUID(string: "2A37"):
        if let heartRate = parseHeartRate(data) {
            print("Heart rate: \(heartRate) bpm")
        }
    case CBUUID(string: "2A19"):
        let batteryLevel = data.first.map { Int($0) } ?? 0
        print("Battery: \(batteryLevel)%")
    default:
        break
    }
}

private func parseHeartRate(_ data: Data) -> Int? {
    guard data.count >= 2 else { return nil }
    let flags = data[0]
    let is16Bit = (flags & 0x01) != 0
    if is16Bit {
        guard data.count >= 3 else { return nil }
        return Int(data[1]) | (Int(data[2]) << 8)
    } else {
        return Int(data[1])
    }
}
写入值
func writeValue(_ data: Data, to characteristic: CBCharacteristic,
                on peripheral: CBPeripheral,
                preferResponse: Bool = true) {
    let type: CBCharacteristicWriteType
    if preferResponse, characteristic.properties.contains(.write) {
        type = .withResponse
    } else if characteristic.properties.contains(.writeWithoutResponse),
              peripheral.canSendWriteWithoutResponse {
        type = .withoutResponse
    } else if characteristic.properties.contains(.write) {
        type = .withResponse
    } else {
        return
    }

    guard data.count <= peripheral.maximumWriteValueLength(for: type) else { return }
    peripheral.writeValue(data, for: characteristic, type: type)
}

// Confirmation callback for .withResponse writes.
func peripheral(
    _ peripheral: CBPeripheral,
    didWriteValueFor characteristic: CBCharacteristic,
    error: Error?
) {
    if let error {
        print("Write failed: \(error.localizedDescription)")
    }
}

// Resume queued .withoutResponse writes here.
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {}
订阅通知
// Subscribe
peripheral.setNotifyValue(true, for: characteristic)

// Unsubscribe
peripheral.setNotifyValue(false, for: characteristic)

// Confirmation
func peripheral(
    _ peripheral: CBPeripheral,
    didUpdateNotificationStateFor characteristic: CBCharacteristic,
    error: Error?
) {
    if characteristic.isNotifying {
        print("Now receiving notifications for \(characteristic.uuid)")
    }
}

外围角色:广播

使用 CBPeripheralManager 从本地设备发布服务。

final class BLEPeripheralManager: NSObject, CBPeripheralManagerDelegate {
    private var peripheralManager: CBPeripheralManager!
    private let serviceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABC")
    private let charUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABD")

    override init() {
        super.init()
        peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
    }

    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        guard peripheral.state == .poweredOn else { return }
        setupService()
    }

    private func setupService() {
        let characteristic = CBMutableCharacteristic(
            type: charUUID,
            properties: [.read, .notify],
            value: nil,
            permissions: [.readable]
        )

        let service = CBMutableService(type: serviceUUID, primary: true)
        service.characteristics = [characteristic]
        peripheralManager.add(service)
    }

    func peripheralManager(
        _ peripheral: CBPeripheralManager,
        didAdd service: CBService,
        error: Error?
    ) {
        guard error == nil else { return }
        peripheralManager.startAdvertising([
            CBAdvertisementDataServiceUUIDsKey: [serviceUUID],
            CBAdvertisementDataLocalNameKey: "MyDevice"
        ])
    }
}

后台 BLE

后台中心模式

将 bluetooth-central 添加到 UIBackgroundModes。在后台:

  • 扫描必须指定一个或多个服务 UUID;使用 nil 扫描仅适用于前台
  • 扫描选项(包括 CBCentralManagerScanOptionAllowDuplicatesKey)均不起作用
后台外围模式

将 bluetooth-peripheral 添加到 UIBackgroundModes。在后台:

  • 如果未启用此模式,应用暂停时发布的服务内容将被禁用
  • 不会广播本地名称
  • 服务 UUID 会移至溢出区域,需要显式扫描服务

状态恢复

状态恢复允许系统在应用因 BLE 事件而终止并重新启动后,重新创建中心或外围管理器。

中心管理器状态恢复
// 1. Create with a restoration identifier
centralManager = CBCentralManager(
    delegate: self,
    queue: nil,
    options: [CBCentralManagerOptionRestoreIdentifierKey: "myCentral"]
)

// 2. Implement the restoration delegate method
func centralManager(
    _ central: CBCentralManager,
    willRestoreState dict: [String: Any]
) {
    if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey]
        as? [CBPeripheral] {
        for peripheral in peripherals {
            // Re-assign delegate and retain
            peripheral.delegate = self
            discoveredPeripheral = peripheral
        }
    }
    let restoredServices = dict[CBCentralManagerRestoredStateScanServicesKey]
        as? [CBUUID]
    let restoredOptions = dict[CBCentralManagerRestoredStateScanOptionsKey]
        as? [String: Any]
    // Resume scanning with restoredServices/restoredOptions if still needed.
}
外围管理器状态恢复
peripheralManager = CBPeripheralManager(
    delegate: self,
    queue: nil,
    options: [CBPeripheralManagerOptionRestoreIdentifierKey: "myPeripheral"]
)

func peripheralManager(
    _ peripheral: CBPeripheralManager,
    willRestoreState dict: [String: Any]
) {
    let services = dict[CBPeripheralManagerRestoredStateServicesKey]
        as? [CBMutableService]
    let advertisement = dict[CBPeripheralManagerRestoredStateAdvertisementDataKey]
        as? [String: Any]
    // Reconnect app state to restored services/advertisement as needed.
}

常见错误

错误修复方法
在 .poweredOn 之前扫描/连接从 centralManagerDidUpdateState 开始 BLE 操作。
未保留已发现的外围设备在连接和发现期间持有强引用。
生产环境扫描传入 nil 服务按功能所需的服务 UUID 进行筛选。
在 didConnect 之前开始发现服务仅在代理回调中推进流程,并处理失败/断开连接的情况。
写入时忽略特征属性或有效负载限制选择受支持的写入类型,遵守 maximumWriteValueLength,并通过 canSendWriteWithoutResponse 判断是否可使用 .withoutResponse。

检查清单

  • 已将 NSBluetoothAlwaysUsageDescription 添加到 Info.plist
  • 所有 BLE 操作均以 centralManagerDidUpdateState 返回 .poweredOn 为前提
  • 发现的外围设备均通过强引用保留
  • 在生产环境中,扫描使用特定的服务 UUID(而非 nil)
  • 在调用 discoverServices 之前设置 CBPeripheralDelegate
  • 读取、写入或启用通知前,检查特征的属性
  • 写入载荷不超过 maximumWriteValueLength(for:)
  • .withoutResponse 写入遵循 canSendWriteWithoutResponse
  • 如有需要,添加后台模式(bluetooth-central 或 bluetooth-peripheral)
  • 如果应用需要支持在 BLE 事件发生时重新启动,则设置状态恢复标识符
  • 使用状态恢复时,实现 willRestoreState 代理方法
  • 发现目标外围设备后停止扫描
  • 处理断开连接,并可选择实现自动重连逻辑
  • 写入类型与特征属性匹配(.withResponse 与 .withoutResponse)

参考资料

评论

支持 Markdown