iOSのBluetooth通信サンプル(セントラル)

## 本記事について

[【M5Stack】段階的に動かして学ぶBluetooth通信](https://qiita.com/Teach/items/629c338da05a3134a1eb)

上記の記事のセントラルを iOS の CoreBluetooth で置き換えた。
その際に学べたことと、コードをまとめる。

## 環境

macOS Venture 13.3 (M2)
Xcode 14.3
iOS 16.5
M5Atom S3 (ペリフェラル)

## 前提

https://qiita.com/Teach/items/a547e1383680c8372339#%E3%83%86%E3%82%B9%E3%83%88%E7%94%A8%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA-%E3%81%A8-macos%E7%94%A8%E3%81%AE%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%9C%E3%82%8B

上記の記事で作った以下のリポジトリを使う。

https://github.com/teach310/SamplePlugin

ロジックを SwiftPackage に書く。

## 準備 Sample 用クラスを作って view から呼び出す

[commit](https://github.com/teach310/SamplePlugin/commit/1cd25e54f811c535a60d02bc27f8475ea0368a5a)

```BLESample.swift
import Foundation

public class BLESample {
    public init() {
    }

    public func helloWorld() {
        print("Hello World!")
    }
}
```

この BLESample クラスにどんどん機能を足していく。

## プロパティー追加

iOS 13　以降では Core Bluetooth を使用するためには`NSBluetoothAlwaysUsageDescription`が必要

[ドキュメント](https://developer.apple.com/documentation/corebluetooth)

![スクリーンショット 2023-07-22 16.07.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/7ccf507c-416b-d7e8-832b-152a27a8c3af.png)


## セントラルデバイスからペリフェラルデバイスを探す

[ペリフェラルのアドバタイズの実装](
https://qiita.com/Teach/items/629c338da05a3134a1eb#%E3%82%B9%E3%83%86%E3%83%83%E3%83%972-%E3%82%BB%E3%83%B3%E3%83%88%E3%83%A9%E3%83%AB%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%81%8B%E3%82%89%E3%83%9A%E3%83%AA%E3%83%95%E3%82%A7%E3%83%A9%E3%83%AB%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%92%E6%8E%A2%E3%81%99m5atomlite)

```BLESample.swift
import Foundation
import CoreBluetooth

public class BLESample: NSObject, CBCentralManagerDelegate {
    var centralManager: CBCentralManager!
    var serviceUUID: CBUUID!
    
    public override init() {
        super.init()
        // queueはnilの場合はメインスレッドで実行される
        // https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1519001-init
        centralManager = CBCentralManager(delegate: self, queue: nil)
        serviceUUID = CBUUID(string: "068c47b7-fc04-4d47-975a-7952be1a576f")
    }
    
    public func scan() {
        if centralManager.state != .poweredOn {
            print("Bluetooth is not powered on.")
            return
        }
        
        if centralManager.isScanning {
            print("Already scanning.")
            return
        }
        
        let options: [String: Any] = [
            CBCentralManagerScanOptionAllowDuplicatesKey: false
        ]
        
        // serviceUUIDを指定すると、指定したサービスを持つペリフェラルのみをスキャンする。(推奨設定)
        // https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518986-scanforperipherals
        centralManager.scanForPeripherals(withServices: [serviceUUID], options: options)
    }
    
    func cbManagerStateName(_ state: CBManagerState) -> String {
        switch state {
        case .unknown:
            return "unknown"
        case .resetting:
            return "resetting"
        case .unsupported:
            return "unsupported"
        case .unauthorized:
            return "unauthorized"
        case .poweredOff:
            return "poweredOff"
        case .poweredOn:
            return "poweredOn"
        @unknown default:
            return "unknown default"
        }
    }
    
    // MARK: - CBCentralManagerDelegate
    
    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print("centralManagerDidUpdateState: \(cbManagerStateName(central.state))")
    }
    
    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        print("Device found: \(peripheral)")
        central.stopScan()
    }
}

```



シミュレーターでは Bluetooth の機能が使用できない(unsupported)ため、動作確認は iOS 実機で行う。
実機の Bluetooth は ON にしておくこと。

まず、アプリを起動した際に以下のログが出る

```
centralManagerDidUpdateState: poweredOn
```

ペリフェラルを起動。
ボタンを押して以下のようにスキャンされたデータがログに出力される

```
Device found: <CBPeripheral: 0x283510000, identifier = 36958114-4913-C3C3-C16F-1CF3831B8211, name = M5AtomS3 BLE Server, mtu = 0, state = disconnected>
```

## ポイント
#### NSObject を継承している理由

ないとエラーでるから。

#### options について

同じデバイスを検知したくはないため明示的に設定、多分デフォルト挙動と同じだが、何がデフォルトなのかを調べる手間をなくすために明示。

#### アクティブスキャンをしないようにする設定は？

ドキュメントを読んでも見つからなかった。

#### delegate (イベントハンドラ)　を centralManagerのインスタンスを持っているクラスと同じにしている理由

1つのクラスで書いた方がシンプルだから。特にこだわりはない。

#### cbManagerStateName メソッドの意味

これがないと state が数字で表示されてなんだかわからないから。
Copilot でサクッと作られる。

## セントラルからペリフェラルに接続する

[diff](https://github.com/teach310/SamplePlugin/commit/c009d32566bd62bc032441427a66879e08690253)

```BLESample.swift
public class BLESample: NSObject, CBCentralManagerDelegate {
    var centralManager: CBCentralManager!
    var serviceUUID: CBUUID!
    var peripheral: CBPeripheral!

...

    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        print("Device found: \(peripheral)")
        self.peripheral = peripheral
        central.stopScan()
        // optionsは特に指定しないといけなそうなものがなかったため、nilを指定している
        // https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518766-connect
        central.connect(peripheral)
    }
    
    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("centralManager:didConnect: \(peripheral)")
    }
    
    public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print("centralManager:didFailToConnect: \(peripheral)")
    }
    
    public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("centralManager:didDisconnectPeripheral: \(peripheral)")
    }
```


```
...
Device found: <CBPeripheral: 0x2827f15f0, identifier = 36958114-4913-C3C3-C16F-1CF3831B8211, name = M5AtomS3 BLE Server, mtu = 0, state = disconnected>
centralManager:didConnect: <CBPeripheral: 0x2827f15f0, identifier = 36958114-4913-C3C3-C16F-1CF3831B8211, name = M5AtomS3 BLE Server, mtu = 23, state = connected>

```

## 接続中かどうかを取得

```ContentView.swift
            Button(action: {
                if bleSample.isConnected() {
                    print("connected")
                } else {
                    bleSample.scan()
                }
```

```BLESample.swift
    public func isConnected() -> Bool {
        if peripheral == nil {
            return false
        }
        return peripheral.state == .connected
    }
```

一つのボタンでタイミングで別の処理を行うために、接続してるかどうかを取れるようにする

[diff](https://github.com/teach310/SamplePlugin/commit/7d59088fea7cfde5e780a3c76d3209e05beefdb2)

## Read

[diff](https://github.com/teach310/SamplePlugin/commit/ff67b13461968e930a6d00aced731cec0a5831d5)

コードの一部

```BLESample.swift
    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("centralManager:didConnect: \(peripheral)")
        
        peripheral.discoverServices([serviceUUID])
    }
    
...
    // MARK: - CBPeripheralDelegate
    
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        let services = peripheral.services ?? []
        print("peripheral:didDiscoverServices: \(services)")
        if let error = error {
            print("error: \(error)")
            return
        }
        
        for service in services {
            peripheral.discoverCharacteristics([characteristicUUID], for: service)
        }
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        let characteristics = service.characteristics ?? []
        print("peripheral:didDiscoverCharacteristicsFor: \(characteristics)")
        if let error = error {
            print("error: \(error)")
            return
        }
        
        for characteristic in characteristics {
            if characteristic.properties.contains(.read) {
                peripheral.readValue(for: characteristic)
            }
        }
    }
    
    // セントラルのread, ペリフェラルのnotifyによりvalueが更新されたときに呼ばれる
    public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        print("peripheral:didUpdateValueFor: \(characteristic)")
        if let error = error {
            print("error: \(error)")
            return
        }
        
        if let data = characteristic.value {
            print("data: \(data)")
            print("string: \(String(data: data, encoding: .utf8) ?? "")")
        }
    }
```

手続き的じゃなく、追いづらいがやってることは以下

```
ペリフェラルに接続
-> サービスを取得
-> キャラクタリスティックを取得
-> キャラクタリスティックの値を読む
-> 読めたらprint
```

```
peripheral:didDiscoverServices: [<CBService: 0x283a694c0, isPrimary = YES, UUID = 068C47B7-FC04-4D47-975A-7952BE1A576F>]
peripheral:didDiscoverCharacteristicsFor: [<CBCharacteristic: 0x280b300c0, UUID = E3737B3F-A08D-405B-B32D-35A8F6C64C5D, properties = 0xA, value = (null), notifying = NO>]
peripheral:didUpdateValueFor: <CBCharacteristic: 0x280b300c0, UUID = E3737B3F-A08D-405B-B32D-35A8F6C64C5D, properties = 0xA, value = {length = 11, bytes = 0x48656c6c6f20576f726c64}, notifying = NO>
data: 11 bytes
string: Hello World

```

## Write

[diff](https://github.com/teach310/SamplePlugin/commit/15878e227a52eb03c8fc4ccda9c55911b72e0a1a)

```BLESample.swift
    public func writeData() {
        if peripheral == nil {
            print("peripheral is nil")
            return
        }

        guard let characteristic = remoteCharacteristic else {
            print("characteristic is nil")
            return
        }

        // ランダムな三桁の数値の文字列を書き込む
        let value = "Write Data " + String(format: "%03d", Int.random(in: 100..<1000))
        // type はwithResponseである必要はないが、取れるようにしておく
        // https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518747-writevalue
        peripheral.writeValue(value.data(using: .utf8)!, for: characteristic, type: .withResponse)
    }

...

    // writeValue を .withResponse で実行した場合に呼ばれる
    public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        print("peripheral:didWriteValueFor: \(characteristic)")
        if let error = error {
            print("error: \(error)")
            return
        }
    }
```

これでセントラルからデータを送信できる

応答がない類のデータに関しては .withResponse ではなく、 .withoutResponse にする

## Notify

[diff](https://github.com/teach310/SamplePlugin/commit/4c133d983904f6f28863fb9055376f92c84651f7)

```BLESample.swift
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
...
        self.remoteCharacteristic = characteristics.first { $0.uuid == characteristicUUID }
        self.notifyRemoteCharacteristic = characteristics.first { $0.uuid == notifyCharacteristicUUID }
        if let notifyRemoteCharacteristic = notifyRemoteCharacteristic {
            // https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518949-setnotifyvalue
            peripheral.setNotifyValue(true, for: notifyRemoteCharacteristic)
        }

...

    public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        print("peripheral:didUpdateNotificationStateFor: \(characteristic)")
        if let error = error {
            print("error: \(error)")
            return
        }
    }

```

```
peripheral:didUpdateValueFor: <CBCharacteristic: 0x283610360, UUID = C9DA2CE8-D119-40D5-90F7-EF24627E8193, properties = 0x10, value = {length = 15, bytes = 0x4e6f74696679204461746120373131}, notifying = YES>
data: 15 bytes
string: Notify Data 711
```

notify されたデータの print は Read と同様のメソっドで実行される