mcpField
C
core-bluetooth

core-bluetooth

github.com/dpearson2699開発1.2k3.8k 回インストール13 週間前に更新
リポジトリ

core-bluetooth について

Swift 開発者向けの Core Bluetooth スキルです。BLE デバイスのスキャン、接続、GATT データ交換に加え、ペリフェラルのアドバタイズ、バックグラウンド動作、状態復元を扱います。

主な機能

  1. 01BLE ペリフェラルのスキャンと接続
  2. 02サービスとキャラクタリスティックの検出
  3. 03データの読み書きと通知の購読
  4. 04ペリフェラルとしてサービスをアドバタイズ
  5. 05バックグラウンド BLE モードの設定
  6. 06Bluetooth マネージャー状態の復元

活用シーン

  1. 01BLE センサーへの接続とデータ読み取り
  2. 02ペリフェラルへのキャラクタリスティック書き込み
  3. 03アプリのバックグラウンドでのスキャンやアドバタイズ
  4. 04BLE イベント後のアプリ状態の復元

インストール

コマンドをコピーしてターミナルで実行します。

$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

Bluetooth Low Energy(BLE)デバイスをスキャンして接続し、データを交換します。 ペリフェラルのスキャンと接続を行うセントラルの役割、サービスをアドバタイズするペリフェラルの役割、バックグラウンドモード、状態の復元について説明します。 プライバシーを保護しながらアクセサリを検出して設定するには accessorysetupkit を使用してください。Core Bluetooth による直接的な GATT 通信には、このスキルを使用してください。

目次

セットアップ

Info.plist キー
キー目的
NSBluetoothAlwaysUsageDescription必須です。アプリが Bluetooth を使用する理由を説明します
UIBackgroundModes with bluetooth-centralバックグラウンドでのスキャンと接続
UIBackgroundModes with bluetooth-peripheralバックグラウンドでのアドバタイズ
Bluetooth の認証

Core Bluetooth には、明示的な許可要求 API はありません。NSBluetoothAlwaysUsageDescription を追加し、アプリが Bluetooth にアクセスする準備ができたらマネージャーを作成してから、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

バックグラウンドのセントラルモード

UIBackgroundModes に bluetooth-central を追加します。バックグラウンドでは、次のようになります。

  • スキャンでは 1 つ以上のサービス UUID を指定する必要があります。nil を使ったスキャンはフォアグラウンドでのみ実行できます
  • CBCentralManagerScanOptionAllowDuplicatesKey を含むスキャンオプションは効果がありません
バックグラウンドのペリフェラルモード

UIBackgroundModes に bluetooth-peripheral を追加します。バックグラウンドでは、次のようになります。

  • このモードがない場合、公開済みサービスの内容はアプリがサスペンドされている間、無効になります
  • ローカル名はアドバタイズされません
  • サービス 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 を守り、.withoutResponse の使用は canSendWriteWithoutResponse で制御します。

レビューチェックリスト

  • Info.plist に NSBluetoothAlwaysUsageDescription を追加している
  • すべての BLE 操作が、centralManagerDidUpdateState の戻り値が .poweredOn の場合にのみ実行される
  • 検出したペリフェラルを強参照で保持している
  • 本番環境でのスキャンに、特定のサービス UUID を使用している(nil ではない)
  • discoverServices を呼び出す前に CBPeripheralDelegate を設定している
  • 読み取り/書き込み/通知の前に、キャラクタリスティックのプロパティを確認している
  • 書き込みペイロードが maximumWriteValueLength(for:) の上限内に収まっている
  • .withoutResponse の書き込みで canSendWriteWithoutResponse を確認している
  • 必要に応じてバックグラウンドモード(bluetooth-central または bluetooth-peripheral)を追加している
  • BLE イベント発生時のアプリ再起動サポートが必要な場合、状態復元識別子を設定している
  • 状態復元を使用する場合、デリゲートメソッド willRestoreState を実装している
  • 対象のペリフェラルを検出した後、スキャンを停止している
  • オプションの自動再接続ロジックを含め、切断を処理している
  • 書き込みタイプがキャラクタリスティックのプロパティ(.withResponse と .withoutResponse)に一致している

参考資料

コメント