CoreBlueToothですれ違い通信を試みる(検証は追記🙇🏻‍♂️)

タイトルにもあるように検証はアドベントカレンダーまでに終了きれなかったので，追記させてもらいます🙇🏻‍♂️
年内にやります，絶対．
# はじめに
こんにちは！メリークリスマス！
アドベントカレンダー最終日を担当させていただきます，しゅうとです！
今年もあっという間に年末がやってまいりましたね．今年は運営活動や就活，ゼミなどで大学生活の中でも一番体感速度が早く，2024年の年越しに誰にもバレないように一人でひっそりとジャンプをしたのが昨日のことのように感じますね❄️

そんなことは置いておいて，今回は今年の春からずっと気になっていた
Swiftの**CoreBlueTooth**と**すれ違い通信**
についてある検証がしたかったのですが，バタバタしていて完全に忘れてしまっていたので，今回のアドベントカレンダーを機会にそれを実際に検証しながらCoreBlueToothの周辺情報もまとめていきたいと思います！
CoreBlueToothの概要については既に超絶わかりやすい記事がたくさんあるので，今回は省略させていただいている部分が多くありますが，ご勘弁ください🙇🏻‍♂️
さらに，素人の興味本位の検証でもあるので，条件や環境がガバガバじゃないかという意見もあるかと思いますが，それもご勘弁ください🙇🏻‍♂️


# 検証内容と動機
まずはじめになぜ今回**どんな検証をしたいのか**についてです．
### 今回の検証内容
- CoreBlueToothを用いたデバイス同士のリアルタイム情報交換の実現性検証
- すれ違い時の**相対速度**，**距離** がどれくらいデバイス同士の通信に影響してくるのかの比較検証
- データの大きさがどれくらいデバイス同士の通信に影響してくるのかの比較検証(追記)

主にこの3点の検証を実際に実装しながら行いたい思います！

**かなり余談(動機)**
動機の部分に関してなのですが，なぜ今回このような検証を行おうと思ったかと言うと，バイク同士がすれ違う時に手を振りあう**YAEH**という文化があるのですが，その文化をスマホアプリに落とし込んで我らがド世代の3DSのすれ違い通信のバイク版を作ったら面白そうだなと思い，今年の春に所属団体のプロジェクト作ろうとした際にCoreBlueToothでできたらベストだよねという話になったのですが，流石に公道の最大速度の60kmですれ違った時に相対速度120kmで通信するのは難しいよねという話になり，結局CoreLocationを用いてゴリ押しで実装したという思い出があり，それを思い返した時にじゃあ実際どれくらいの速度，データ量だったらいけるんだろうという疑問が思い浮かび，検証しようと思ったのが，動機になります！(丁度団体の後輩が同じような実装をしようとしていて参考になればいいなという気持ちも半分)

# 検証方法
検証する方法に関して今回は以下のような条件，状況で検証していきたいと思います．
### 条件
- **使用機種**: iPhone13
- **実験場所**: 立命館大学内(人が少なく静か目な場所)
### 状況
- **すれ違いスピード**
    - 時速０km(静止状態)
    - (推定)時速1.8km(一般の人がかなりゆっくり歩いた場合)
    - (推定)時速3.6km(一般の人がゆっくり歩いた場合)
    - (推定)時速8km(一般の人が早歩きをした場合)
    - (推定)時速13km(一般の人が走りをした場合)
    - 時速30km(原付の法定速度)
    - 時速60km(普通自動車，一般道の法定速度)

方法としては
上記の条件で2人が歩く，走る，バイクを走らせるなどをして，専用のアプリケーション(仕様は後述)でアドバタイズ，スキャニングを行いデバイス間で接続，データが交換できるかを検証する.
以上の条件，環境で検証していきたいと思います！

ではまず実装部分から参ります！

# アプリケーションの実装

まずはざっくり今回のデータ送受信までの流れを書くと．．．
1. アプリケーション起動時デバイス1, 2がアドバタイズを発信し，同時にスキャニングを開始！！
2. デバイス1, 2がそれぞれのアドバタイズを検知し接続！！
3. 各デバイスのセントラルが接続したペリフェラルの指定したサービス，指定したデータを取得しに行く！！
3. データを交換！！

という流れになっています！
画面としては以下のようになっており，今の接続状況やログを見れるようにシンプルなListで表示しています．
下のボタンを押すと，今繋がっているデバイスがListに追加されるようになっています．
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058061/d8819265-337f-d60e-58d1-14589ccf8da7.jpeg" width="50%">

今回は最小限の機能のみを実装していくので，構成として
```PeripheralManager```
```CentralManager```
```CBTthVerificationView```
```CBTthVerificationViewModel(なんちゃってVM)```

で作成したいと思います！
GitHubリポジトリはこちら

https://github.com/sawakishuto/CBTVerif

まずは```PeripheralManager```の実装です!

# PeripheralManager

先にコードの全体像を載せておきます！

<details><summary>PeripheralMagnagerクラス</summary>

```swift
import Combine
import CoreBluetooth
import Foundation

class PeripheralManager: NSObject, CBPeripheralManagerDelegate {
    var peripheralManager: CBPeripheralManager!
    var transferCharacteristic: CBMutableCharacteristic?
    var peripheralPublisher = PassthroughSubject<String, Never>()

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

    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        if peripheral.state == .poweredOn {
            print("Peripheral is powered on")
            setupService()
            startAdvertising()
        } else {
            print("Peripheral state: \(peripheral.state.rawValue)")
        }
    }

    func setupService() {
        transferCharacteristic = CBMutableCharacteristic(
            type: CBUUID(string: "180C"),
            properties: [.read],
            value: nil,
            permissions: [.readable]
        )

        let service = CBMutableService(type: CBUUID(string: "180B"), primary: true)
        service.characteristics = [transferCharacteristic!]

        peripheralManager?.add(service)
    }

    func startAdvertising() {
        let advertisementData: [String: Any] = [
            CBAdvertisementDataLocalNameKey: "Device",
            CBAdvertisementDataServiceUUIDsKey: [CBUUID(string: "180A")]
        ]
        peripheralManager?.startAdvertising(advertisementData)
    }

    func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
        if let error = error {
            print("Failed to start advertising: \(error.localizedDescription)")
        } else {
            print("Advertising started successfully.")
            peripheralPublisher.send("Peripheral started advertising")
        }
    }

    func peripheralManager(
        _ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest
    ) {
        if request.characteristic.uuid == transferCharacteristic?.uuid {
            request.value = "UidIs1234".data(using: .utf8)
            peripheralManager?.respond(to: request, withResult: .success)
        }
    }
}
```

</details>
早速サクッと紹介します！

今回の実装で注目したいのが，こちらのクラスです．

``` CBMutableCharacteristic```
と
```CBMutableService```です.

それぞれについて説明すると

```CBMutableCharacteristic```とは
---
***GATTサーバー***(通信をする際にデータを提供する役割を持つデバイスやソフトウェア)
が定義する編集可能な```Characteristic```です．
超簡単にいうとここに送信したいデータ(UUID，編集権限，データの初期値，操作可能性)などを設定し格納し，これを***GATTクライアント***は読み取りの要求を行いデータを受け取るという流れになっています．

https://developer.apple.com/documentation/corebluetooth/cbmutablecharacteristic

https://blog.reinforce-lab.com/post/2013-01-28-cbcharacteristics_class_ref_jp/


:::note info
そもそも```Characteristic```って何？
```Characteristic```とはBLEデバイスと通信する際の構成要素の一つであり．特定のデータとそれに関連するデータを含みます.
そして，一つの```Characteristic```は一つ以上の```Service(後述)```に関連付けられる．
<details><summary>Characteristic構成要素</summary>

- ```UUID```: 
    Characteristicを識別する際に使用(セントラルクラスでUUIDを指定して読み取るデータなどを指定する．)
- ```プロパティ```: 
    - ```read```: データを読み取れる.
    - ```write```: データを書き込める.
    - ```notify```: データの変更を通知できる.
    - ```indicate```: データ変更通知を確認応答付きで行える.
- ```値 (Value)```
    Characteristicが保持するデータ本体
</details>

:::

今回は```setupService```関数内で
```swift
 transferCharacteristic = CBMutableCharacteristic(
            type: CBUUID(string: "180C"),
            properties: [.read],
            value: nil,
            permissions: [.readable]
        )
```
とあるように```UUID```に**180D**, **読み取り専用**,初期値は**nil**で定義してあります．


```CBMutableService```とは
---

```CBMutableService``` は ペリフェラルで公開するカスタムサービスを構成するためのクラスです．
先述した通り，一つの```Characteristic```は一つ以上の```Service```に関連付けられるので，これも超簡単にいうと先ほど定義した```transferCharacteristic```を```Service```に込めてデバイス同士の接続が完了した後にぶん投げるというイメージ．
今回は以下のように```180B```という識別子をつけ，transferCharacteristicを登録しています．
```swift
let service = CBMutableService(type: CBUUID(string: "180B"), primary: true)
service.characteristics = [transferCharacteristic!]
```

:::note info
**ServiceのUUIDが2つ出てくるねんけどぉ**
```Peripheral```クラスをみるとお気づきの通り2つの```Service```にCBUUIDを付与しています．
この二つの```ServiceUUID```の違いを簡単に説明すると
```swift
CBAdvertisementDataServiceUUIDsKey: [CBUUID(string: "180A")]
```
上記のUUIDは「俺ここにいるよ！！」とみんなに発信する，**アドバタイズパケット**の識別子となっており，デバイスの**接続前**にセントラルは自分が知っている識別子とこの識別子が合致するものに対して接続を開始します．
```swift
CBMutableService(type: CBUUID(string: "180B"), primary: true)
```
そして今回紹介したこちらのUUIDはデバイスの**接続後**に特定のデータが入った**Service**を見つけるためのUUIDであり，接続が完了するとセントラルは自分が知っている識別子と同じ識別子を持つサービスを見つけ，欲しいデータを取得します．

つまり
**CBAdvertisementDataService** はセントラルが接続したいアドバタイズを見つけるためのUUID
**CBMutableService**は接続後にセントラルがほしいデータが入ったサービスを見つけるためのUUID
のようなイメージを持っていただければと思います
:::

そしてこれらの設定が完了したら，実際に```startAdvertising(advertisementData)```関数でアドバタイズを発信します．
```swift
    func startAdvertising() {
        let advertisementData: [String: Any] = [
            CBAdvertisementDataLocalNameKey: "Device",// デバイスの名前(セントラルがアドバタイズを見つけたときこのKeyが取得できる)
            CBAdvertisementDataServiceUUIDsKey: [CBUUID(string: "180A")]
        ]
        peripheralManager?.startAdvertising(advertisementData) // アバタイズを発信する際のデバイス名とUUIDを込めて発信を始める．
    }
```

PeripheralManagerクラスの最後の
```func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request:CBATTRequest)```についてです．
このデリゲートメソッドはセントラルが無事ペリフェラルと接続でき，サービスのUUIDを用いてペリフェラルの特定のサービスを特定し，さらにその中の特定の```Characteristic```を特定し送信要求をしてきたときに呼び出されるメソッドで，内容としては
```swift
        if request.characteristic.uuid == transferCharacteristic?.uuid {
            request.value = "UidIs1234".data(using: .utf8)
            peripheralManager?.respond(to: request, withResult: .success)
        }
```
送られてきた要求(request)の特定の```Characteristic```(request.characteristic)に含まれるUUIDがペリフェラルが設定したものと一致していれば，requestのvalueに特定のデータを格納し，返すというようになっています！←ここにUserIDとか入れたらデータを交換できるのでは？という考えで，今回はValueに個々のUserIDを格納することを想定して検証していきます！(セキュリティ的な面はあまり考慮できていないかもです？？)
```PeripheralManager```クラスの解説は以上になります！

---


# CentralManager

こちらも全体像を先に載せます！

<details><summary>CentralManagerクラス</summary>

```swift

class CentralManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
    var centralManager: CBCentralManager?
    var centralPublisher = PassthroughSubject<String, Never>()
    var discoveredPeripheral: CBPeripheral? // Peripheralを保持するプロパティ

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

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            print("Central is powered on")
            centralPublisher.send("Central is powered on")
            startScanning()
        } else {
            print("Central state: \(central.state.rawValue)")
        }
    }

    func startScanning() {
        centralManager?.scanForPeripherals(withServices: [CBUUID(string: "180A")], options: nil)
    }

    func centralManager(
        _ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
        advertisementData: [String: Any], rssi RSSI: NSNumber
    ) {
        if RSSI.intValue > -100 {
            let deviceName = peripheral.name ?? "Unknown Device"
            print("Discovered peripheral: \(deviceName)")
            centralManager?.stopScan()
            discoveredPeripheral = peripheral
            discoveredPeripheral?.delegate = self // delegateを設定
            centralManager?.connect(peripheral)
            centralPublisher.send("connection complete peripheral \(deviceName)")
        }
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("Connected to peripheral: \(peripheral.name ?? "Unknown")")
        peripheral.discoverServices([CBUUID(string: "180B")])
    }


    func peripheral(
        _ peripheral: CBPeripheral, didDiscoverServices error: Error?
    ) {
        if let services = peripheral.services {
            for service in services {
                centralPublisher.send("Discovered Service: \(service.uuid)")
                if service.uuid == CBUUID(string: "180B") {
                    peripheral.discoverCharacteristics([CBUUID(string: "180C")], for: service)
                }
            }
        }
    }

    func peripheral(
        _ peripheral: CBPeripheral,
        didDiscoverCharacteristicsFor service: CBService, error: Error?
    ) {
        if let characteristics = service.characteristics {
            for characteristic in characteristics {
                if characteristic.uuid == CBUUID(string: "180C") {
                    print("Discovered Characteristic: \(characteristic.uuid)")
                    centralPublisher.send("Discovered Characteristics")
                    peripheral.readValue(for: characteristic)
                }
            }
        }
    }

    func peripheral(
        _ peripheral: CBPeripheral,
        didUpdateValueFor characteristic: CBCharacteristic, error: Error?
    ) {
        centralPublisher.send("readValue")
        if let value = characteristic.value {
            let receivedString = String(data: value, encoding: .utf8) ?? "Invalid data"
            print("Received from Peripheral: \(receivedString)")
            centralPublisher.send(receivedString)
        }
    }
}
```

</details>

セントラルのクラスでは基本的に
1. ペリフェラルで実装したアドバタイズをスキャニング
2. 特定のUUIDを持ったサービスを見つけ接続(スキャニングをストップ)
3. 接続先のペリフェラルに含まれるサービスの中から指定のUUIDを持つものを探す
4. 特定したサービスの中から指定したUUIDの```Characteristics```を探し，見つけた場合は取得要求を送信
5. ペリフェラルからデータを受け取り
という流れになっております！


こちらもざっくり解説していきます！

まずはこちらのデリゲートメソッドについてです．
```swift
    func centralManager(
        _ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
        advertisementData: [String: Any], rssi RSSI: NSNumber
    ) {
        if RSSI.intValue > -100 {
```
こちらはスキャニングを開始した後に実際に指定したUUIDのアドバタイズパケットを見つけた時に発火する関数になります．そして少し注目したいのが，
```rssi RSSI: NSNumber```こちらの引数にはRSSIと呼ばれるセントラルとペリフェラルの**デバイス間の電波強度**を表す数値が入ってきます．つまり今回はアドバタイズを見つけた時にこのメソッドが発火されるので，アドバタイズを見つけた時のデバイス間の電波強度が入ってくるということになります．
さらに今回は**RSSI**の値が-100以上(通信強度としては低いが，今回の検証ではちょうど良かったので，調整しました)であることを条件としているので，ある程度電波強度が強い時のみに接続をするというロジックになっています．(つまり，ある程度近い距離ですれ違った場合のみに接続し，データを送る)

さらにそれぞれのデリゲートメソッドについて見ていきましょう．

まずこちらです．
```swift
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
```
この関数は無事ペリフェラルと接続できた時に呼び出される関数になります，
ここでは実際に指定したUUID("180B")を**ペリフェラル**に対して「このUUIDのサービス探してくれ！」とお願いします．
```swift
        peripheral.discoverServices([CBUUID(string: "180B")])
```

こちらは先ほど記述したデリゲートメソッドでペリフェラルがサービスを発見したときにに発火する関数です．
**didDiscoverServices**には先ほどの関数でペリフェラルに対して探索のお願いをしてその結果の内容が入っています．
この関数内では，指定したUUIDを元に```Characteristics```を探させる関数を発火させます．
この関数ないでサービスのUUIDを改めて```if let```で定義し直しているのはもしかしたらそれ以外の値も入っているかも．．．というリスクを回避するためっぽいですね．
```swift
func peripheral(
        _ peripheral: CBPeripheral, didDiscoverServices error: Error?
    )
```
ここでもペリフェラルに対して「"180C"をUUIDとするCharacteristicはないか探してくれ！」とお願いします．
```swift
peripheral.discoverCharacteristics([CBUUID(string: "180C")], for: service)
```

ここまで見るともうお気づきだと思いますが，こちらは```Characteristics```を見つけた時に発火する関数です．
ここでは先ほどと同じく，実際に帰ってきたデータが正しいかの妥当性を確認します．
```swift
func peripheral(
        _ peripheral: CBPeripheral,
        didDiscoverCharacteristicsFor service: CBService, error: Error?
    )
```
この関数内で実際に欲しいデータがあった場合にペリフェラルに対して読み取りを要求します．

> if request.characteristic.uuid == transferCharacteristic?.uuid {
            request.value = "UidIs1234".data(using: .utf8)
            peripheralManager?.respond(to: request, withResult: .success)
        }
送られてきた要求(request)の特定のCharacteristic(request.characteristic)に含まれるUUIDがペリフェラルが設定したものと一致していれば，requestのvalueに特定のデータを格納し，返すというようになっています！

この処理に移るというわけですね！
```peripheral.readValue(for: characteristic)```

そしてデータが帰ってきたらこの関数が発火し，データを表示すしてくれるという流れです！！
```swift
func peripheral(
        _ peripheral: CBPeripheral,
        didUpdateValueFor characteristic: CBCharacteristic, error: Error?
    )
```

ここまでが，実装の流れになります！！
その他のクラスの実装は省略させていただきますが，
各Managerクラスの各ポイントでログをCombineで購読し配列に追加し，その配列をViewとバインディングする形で画面に表示しています！

---

さて，ささっと解説する予定だったのに超絶ボリューミーになってしまいましたが，こっから実際に検証に写っていきたいと思います！！

# 検証(途中)🚧
では早速検証していきましょう！

今回の条件としてアドバタイズを見つけ，デバイス間のRSSIが-100dBm以上になった場合,接続を開始するという前提で行なっていきます！
今回はすれ違う前のスタート時の2人の距離として，2人が同時にアドバタイズ，スキャニングを開始したタイミングで，接続されないことを確認できる距離(だいたい50m程度)で行いました．

**検証結果に移ります！！！**

### 時速０km(静止状態&デバイスを隣に置く)の場合
ちゃんと実装として合っているかを確認するための実現性検証みたいなものですね．
この場合は通信を開始したとほぼ同時に接続されました．

### (推定)時速1.8km(一般の人がかなりゆっくり歩いた場合)
この場合は


# 最後に
かなり長い記事になってしまいましたが，最後まで読んでいただきありがとうございました！
初めてCoreBlueToothをがっつり触ったので，よりデバイスと仲良くなれた気がしてとても楽しかったです！実装としてもかなりガバガバなものではあると思いますが，前から気になっていた疑問点を検証することができて良かったです！
間違っている箇所やもっと良くできる部分などあればぜひ教えていただきたいです！
2024年も残り6日となりましたが，
お体にお気をつけて，良いお年🍊&メリークリスマス🧑‍🎄