【Swift5】Bluetoothクラス実装の備忘録

## はじめに

iOSでBluetooth接続処理を実装したときの備忘録です。

## 環境
- Xcode10.3
- Swift5

##概要

```.swift
import CoreBluetooth
```

端末検索〜接続、操作までCoreBluetoothで全て実装することができました。

##主役となるManagerクラス

- [CBCentralManager](https://developer.apple.com/documentation/corebluetooth/cbcentralmanager)・・・アドバタイズされた周辺機器のスキャン、検出、接続や、検出または接続されたリモート周辺機器の管理に使用されます。

- [CBPeripheralManager](https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager)・・・ローカル周辺機器のGeneric Attribute Profile（GATT）データベース内で公開されたサービスを管理し、これらのサービスをアドバタイズするために使用されます。

<img width="565" alt="スクリーンショット 2019-09-02 8.17.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186535/13a9b3b1-d919-b6a7-981c-cefca70e86b2.png">


<img width="545" alt="スクリーンショット 2019-09-03 9.09.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186535/8161c6eb-981f-b669-8cbc-88a0d45c9942.png">

なお、Bluetoothの操作は、主に以下のデリゲートメソッドを使用して行います。

- [CBCentralManagerDelegate](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate)
- [CBPeripheralDelegate](https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate)

##実装フロー

>①セントラルマネージャーを起動する。
②ペリフェラルを検出する。
③ペリフェラルと接続する。
④ペリフェラルのサービスを検出する。
⑤サービス特性を検出する。
⑥サービス特性の値を取得する。
引用：[iOS SwiftでBLEのサンプルを動かしてみる](https://qiita.com/eKushida/items/def628e0eff6c106d467)

<img width="709" alt="スクリーンショット 2019-10-01 1.27.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/186535/bb836af6-4826-20fe-9a4f-8e067c44a1b5.png">

## BluetoothServiceクラスの実装

今回は、Constに使用するサービスのUUIDを定義し、
スキャン時には、タイムアウトエラー実装用に、タイマーを設定するようにしました。
なお、対象のペリフェラルの検出には、ペリフェラルの名前を指定するようにしています。

```Const.swift
struct Const {

    struct Bluetooth {
        /// サービスのUUID
        struct Service {
            static let kUUID: String = "cd000000-1234-1234-1234-hogehogehoge"
        }
        
        /// サービスのキャラクタリスティックのUUID
        struct Characteristic {
            static let kUUID01 = "cd100001-1234-1234-1234-hogehogehoge"
            static let kUUID02 = "cd100002-1234-1234-1234-hogehogehoge"
        }
        
        static let kPeripheralName = "Hoge Bluetooth"
    }

}
```

```BluetoothService.swift
final class BluetoothService: NSObject {
    /// 接続先の機器
    private var connectPeripheral: CBPeripheral? = nil
    /// 対象のキャラクタリスティック
    private var writeCharacteristic: CBCharacteristic? = nil

    override init() {
        self.centralManager = CBCentralManager()
        self.peripheralManager = CBPeripheralManager()
    }
    
    // MARK: - Public Methods
    
    /// Bluetooth接続のセットアップ
    func setupBluetoothService() {
        self.centralManager = CBCentralManager(delegate: self, queue: nil)
        self.peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
    }
    
    /// スキャン開始
    func startBluetoothScan() {
        print("スキャン開始")
        // タイマーを設定する
        self.scanTimer = Timer.scheduledTimer(timeInterval: TimeInterval(10),
                                              target: self,
                                              selector: #selector(self.timeOutScanning),
                                              userInfo: nil,
                                              repeats: false)
        // 機器を検出
        if self.centralManager.isScanning == false {
            self.centralManager.scanForPeripherals(withServices: nil, options: nil)
        }
    }
    
    /// スキャン停止
    func stopBluetoothScan() {
        self.centralManager.stopScan()
        // Timerを削除
        self.scanTimer?.invalidate()
        self.scanTimer = nil
    }
    
    /// 機器に接続
    func connectPeripheral() {
        guard let connectPeripheral = self.connectPeripheral else {
            // 失敗処理
            return
        }
        self.centralManager.connect(connectPeripheral, options: nil)
    }
}
```

## CBCentralManagerDelegate

```BluetoothService.swift
// MARK: CBCentralManagerDelegate

extension BluetoothService: CBCentralManagerDelegate {

    /// Bluetoothのステータスを取得する(CBCentralManagerの状態が変わる度に呼び出される)
    ///
    /// - Parameter central: CBCentralManager
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOff:
            print("Bluetooth PoweredOff")
            break
        case .poweredOn:
            print("Bluetooth poweredOn")
            break
        case .resetting:
            print("Bluetooth resetting")
            break
        case .unauthorized:
            print("Bluetooth unauthorized")
            break
        case .unknown:
            print("Bluetooth unknown")
            break
        case .unsupported:
            print("Bluetooth unsupported")
            break
        }
    }
    
    /// スキャン結果取得
    ///
    /// - Parameters:
    ///   - central: CBCentralManager
    ///   - peripheral: CBPeripheral
    ///   - advertisementData: アドバタイズしたデータを含む辞書型
    ///   - RSSI: 周辺機器の現在の受信信号強度インジケータ（RSSI）（デシベル単位）
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        // 対象機器のみ保持する
        if let peripheralName = peripheral.name,
            peripheralName.contains(Const.Bluetooth.kPeripheralName) {
            // 対象機器のみ保持する
            self.connectPeripheral = peripheral
            // 機器に接続
            print("機器に接続：\(String(describing: peripheral.name))")
            self.centralManager.connect(peripheral, options: nil)
        }
    }
    
    /// 接続成功時
    ///
    /// - Parameters:
    ///   - central: CBCentralManager
    ///   - peripheral: CBPeripheral
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("接続成功")
        self.connectPeripheral = peripheral
        self.connectPeripheral?.delegate = self
        // 指定のサービスを探索
        if let peripheral = self.connectPeripheral {
            peripheral.discoverServices([CBUUID(string: Const.Bluetooth.Service.kUUID)])
        }
        // スキャン停止処理
        self.stopBluetoothScan()
    }
    
    /// 接続失敗時
    ///
    /// - Parameters:
    ///   - central: CBCentralManager
    ///   - peripheral: CBPeripheral
    ///   - error: Error
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print("接続失敗：\(String(describing: error))")
    }
    
    /// 接続切断時
    ///
    /// - Parameters:
    ///   - central: CBCentralManager
    ///   - peripheral: CBPeripheral
    ///   - error: Error
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("接続切断：\(String(describing: error))")
    }

}
```

## CBPeripheralDelegate

```BluetoothService.swift
// MARK: CBPeripheralDelegate

extension WearBluetoothService: CBPeripheralDelegate {
    
    /// キャラクタリスティック探索時(機器接続直後に呼ばれる)
    ///
    /// - Parameters:
    ///   - peripheral: CBPeripheral
    ///   - error: Error
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard error == nil else {
            // スキャン停止処理
            self.stopScan()
            // 失敗処理
            return
        }
        
        if let peripheralServices = peripheral.services {
            for service in peripheralServices where service.uuid == CBUUID(string: Const.Bluetooth.Service.kUUID) {
                print("キャラクタリスティック探索")
                // キャラクタリスティック探索開始
                let characteristicUUIDArray: [CBUUID] = [CBUUID(string: Const.Bluetooth.Characteristic.kUUID01),
                                                         CBUUID(string: Const.Bluetooth.Characteristic.kUUID02)]
                peripheral.discoverCharacteristics(characteristicUUIDArray, for: service)
            }
        }
    }
}
```




## CBPeripheralManagerDelegate

```BluetoothService.swift
// MARK: CBPeripheralManagerDelegate

extension WearBluetoothService: CBPeripheralManagerDelegate {
    
    /// 端末のBluetooth設定を取得(WearBluetoothServiceの使用開始時、端末のBluetooth設定変更時に呼ばれる)
    ///
    /// - Parameter peripheral: CBPeripheralManager
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        if peripheral.state == .poweredOn {
            // サービスを登録
            let service = CBMutableService(type: CBUUID(string: Const.Bluetooth.Service.kUUID), primary: true)
            self.peripheralManager.add(service)
        }
    }
    
    /// キャラクタリスティック発見時(機器接続直後に呼ばれる)
    ///
    /// - Parameters:
    ///   - peripheral: CBPeripheral
    ///   - service: CBService
    ///   - error: Error
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard error == nil else {
            // スキャン停止処理
            self.stopScan()
            print("キャラクタリスティック発見時：\(String(describing: error))")
            // エラー処理
            return
        }
        guard let serviceCharacteristics = service.characteristics else {
            // スキャン停止処理
            self.stopScan()
            // エラー処理
            return
        }
        // キャラクタリスティック別の処理
        for characreristic in serviceCharacteristics {
            if characreristic.uuid == CBUUID(string: Const.Bluetooth.Characteristic.kUUID01) {
                // データ書き込み用のキャラクタリスティックを保持
                self.writeCharacteristic = characreristic
                print("Write 01")
            }
            if characreristic.uuid == CBUUID(string: Const.Bluetooth.Characteristic.kUUID02) {
                isNotDiscoverCharacteristic02 = false
                peripheral.setNotifyValue(true, for: characreristic)
                print("Notify 02")
            }
        }
    }
    
    /// キャラクタリスティックにデータ書き込み時(コマンド送信時に呼ばれる)
    ///
    /// - Parameters:
    ///   - peripheral: CBPeripheral
    ///   - characteristic: CBCharacteristic
    ///   - error: Error
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        guard error == nil else {
            print("キャラクタリスティックデータ書き込み時エラー：\(String(describing: error))")
            // 失敗処理
            return
        }
        // 読み込み開始
        peripheral.readValue(for: characteristic)
    }
    
    /// キャラクタリスティック値取得・変更時(コマンド送信後、受信時に呼ばれる)
    ///
    /// - Parameters:
    ///   - peripheral: CBPeripheral
    ///   - characteristic: CBCharacteristic
    ///   - error: Error
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        guard error == nil else {
            print("キャラクタリスティック値取得・変更時エラー：\(String(describing: error))")
            // 失敗処理
            return
        }
        guard let data = characteristic.value else {
            // 失敗処理
            return
        }
        // データが渡ってくる
        print(data)
    }
}
```
##まとめ
Dalegateメソッドの呼ばれ順(実際処理のフロー)は、docになるべく記載するようにしました。
CoreBluetoothを扱うに当たって、Dataの変換周りに関しても勉強になったので、近々まとめられたらと思います。


#参考

- [iOS SwiftでBLEのサンプルを動かしてみる](https://qiita.com/eKushida/items/def628e0eff6c106d467)
- [CoreBluetoothを使ってBLE Central側を実装する](https://tech.mokelab.com/ios/CoreBluetooth/BLE/central.html)
- [【Swift3】CoreBluetoothサンプル集](https://qiita.com/nave421m/items/639a7a043844acda4eff)