アプリでタイヤの空気圧を管理しよう

元社員ですが、[第二のドワンゴ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dwango2)の19日目の記事です。

フィアットパンダという化石のようなクルマに乗っているのですが、普段は行動範囲の狭さからあまり乗る機会がありません。たまに乗るとなんとなく乗り心地がおかしいと感じることがあって、大体そういうときはタイヤの空気圧がだいぶ下がっています。
![IMG_6697.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4640/18126112-51bd-f46d-595f-6d2c182c0bd6.jpeg)
当然適正ではない空気圧の状態で運転するのは危険ですので、なるべく乗るたびに空気圧を計測して必要であれば補充するわけですが、毎度4つのタイヤ全ての空気圧を計るのは面倒ですし、現実的ではありません。

そこで今回はタイヤの空気圧監視システム(TPMS)を作ってみることにしました。

## BLE対応 TPMS
Amazonをのぞいてみると、スマホに対応したBLEのTPMSのセットがいくつか販売されています。

https://www.amazon.co.jp/gp/product/B078LX2V18

これを買って対応アプリ入れれば終わりなのですが、それだとqiitaに書く意味ないですし、せっかくなので以前[作成したスマートキーのアプリ](https://note.com/saiten/n/n7ba60f5a7c22)にTPMSの機能を組み込みたいところです。

今回購入したセンサはタイヤのバルブに取り付ける形状となっており、元からある蓋を外してセンサを取り付けるだけで計測できるようになっています。取り外している状態では電波を発しませんが、バルブに取り付けて圧力に変化があった際に、アドバタイズデータを発信するようです。

アドバタイズデータ内のManufacturerData内に、デバイスの識別子と圧力、温度情報が全て含まれており、接続を行ったりすることなくアドバタイズデータを受信するだけで圧力情報を受け取ることができます。

実際に各センサを取り付けてデータを取得した結果が以下の通りです。

```
フロント右側
000181ea ca200372 0c030400 1a080000 0701
```

```
フロント左側
000180ea ca100516 29c00300 6d080000 0300
```

```
リア右側
000183ea ca40000d bfe00200 150a0000 0900
```

```
リア左側
000182ea ca30037a 0b9e0300 4f080000 0300
```

先頭5バイトは全て共有で、その次の３バイトはセンサに付属していた識別子IDの内容と同じになっています。このことからこの3バイトを利用してどのセンサから発信されたデータか区別することができそうです。

次に同じセンサから何度か付けたり外したりを繰り返してデータを取得した結果が以下になります。

```
000182ea ca30037a 36890300 d3060000 0300
000182ea ca30037a 6f3e0300 c6060000 0301
000182ea ca30037a 49430300 da060000 0301
000182ea ca30037a 16150300 e3060000 0301
```

9バイト目から12バイト目までと、13バイト目から16バイト目が変化しています。(17バイト目と18バイト目も変化していますがちょっと謎) それぞれをリトルエンディアンと考えて10進で表すと以下のような形になります。

```
231734 1747
212591 1734
213833 1754
202006 1763
```

左側は圧力(hPa)、右側は100で割ると17度程度になることから温度を表していそうです。

## 実装
実装は対象となるサービスを持つペリフェラルデバイスをスキャンし、アドバタイズデータをパースするだけなので非常にシンプルです。

```swift
let manager = CentralManager(queue: queue, options: [:])
let tireState = manager.observeState())
    .map { $0 == .poweredOn }
    .flatMapLatest { scannable -> Observable<ScannedPeripheral> in
        guard scannable else { return .empty() }
        return manager.scanForPeripherals(withServices: [.tirePressureManagementService])
    }
    .compactMap {
        guard let manufacturerData = $0.advertisementData.manufacturerData else { return nil }
        return TireState(manufacturerData: manufacturerData)
    }
```

```swift
public struct TireState {
    public var position: Position
    public var pressure: Float
    public var temperature: Float
    
    public init?(manufacturerData: Data) {
        guard manufacturerData.count >= 16 else { return nil }

        var deviceID: UInt32 = 0
        let _ = withUnsafeMutableBytes(of: &deviceID) { manufacturerData[5..<8].copyBytes(to: $0) }

        let pressure = manufacturerData[8..<12].withUnsafeBytes { $0.load(as: UInt32.self) }
        let temperature = manufacturerData[12..<16].withUnsafeBytes { $0.load(as: UInt32.self) }
        
        guard let position = Position(rawValue: deviceID) else { return nil }

        self.position = position
        self.pressure = Float(pressure) / 1000.0
        self.temperature = Float(temperature) / 100.0
    }
}

extension TireState {
    public enum Position: UInt32 {
        case frontLeft  = 0x160510
        case frontRight = 0x720320
        case rearLeft   = 0x7A0330
        case rearRight  = 0x0D0040
    }
}
```

こうして取得した値を画面に反映させ、実際に車にセンサーを取り付けたところ全て表示できました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4640/62ab0698-4cdc-a6be-ff8e-eaf8d7676bd8.png" width="50%" />
(空気圧低すぎですね)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4640/8e16f2a6-78eb-51bb-de90-fce0f6f1fc43.jpeg" width="50%" />
ここから空気を補充して(xiaomiの電動空気入れ便利です)、230kPhまで補充して改めてセンサを取り付けると、正しく反映されています。センサの値もそれなりに正確そうです。ここから一定の値まで空気圧が下がったら通知を送るなどすれば実用するところまで持っていけそうですね。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4640/d9bf24e2-316c-895f-c460-7fc87efcdd1f.png" width="50%" />

## まとめ
自作のアプリにタイヤの空気圧を表示する実装を行いました。これ以外にも色々なセンサを取り付けて車体管理できるようにしたら面白そうです。