スマートベッド nerum app を買ったので、レビューとハックする(ハック編)

# TL;DR
* 電動スマートベッド [nerum app](https://www.i-office1.net/shop/36112667/)を買った 
  非常に良い。前編 レビュー編は [こちらへ](https://qiita.com/HiroyukiSakoh/items/4223397685056ab9d3d3)
* BLE通信をスニッファでキャプチャしてプロトコル解析した。
* スマホアプリでも出来ない、照明の状態と角度の表示、HEAD/FOOT同時操作までできるようになった。イエイ
* サンプルコードは全部 C#

# 環境
* Windows 11 Pro 21H2
* Bluetoothドングル サンワサプライ [MM-BTUD46](https://www.sanwa.co.jp/product/syohin?code=MM-BTUD46)
* Bluetoothスニッファ Adafruit nRF51822搭載 Bluefruit LE Sniffer(v2) [amazonで購入した](https://www.amazon.co.jp/dp/B00SKWGPE0)
* Wireshark v3.6.5
* Python 3.10
* VisualStudio 2022 17.2.3
* スマホアプリ [Google Play](https://play.google.com/store/apps/details?id=com.okin.bedding.nerum)、[App Store](https://apps.apple.com/jp/app/nerum/id1564619335)

# BLE

まずは、スニッファを使わず、PCから普通のBLEのデバイスとして様子を確認する。

## アドバタイズ

### サンプルコード C#
* [サンプルコード](https://github.com/HiroyukiSakoh/NerumAppBle/blob/main/BleExample/%E3%82%A2%E3%83%89%E3%83%90%E3%82%BF%E3%82%A4%E3%82%BA%E5%8F%97%E4%BF%A1%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB.cs)
* ポイント
  * ターゲットフレームワーク
    csprojで、次のように指定する
    `<TargetFramework>net6.0-windows10.0.22000.0</TargetFramework>`
  * [BluetoothLEAdvertisementWatcher](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher?view=winrt-22000)クラスを使ってアドバタイズを受信する
  * 目的のServiceUuidが分かっていれば、あらかじめフィルタもかけられる

* パッシブのアドバタイズ
```
AdvertisementType:ConnectableUndirected
Address:30E283XXXXXX Type:Public
LocalName:
Flags:GeneralDiscoverableMode, ClassicNotSupported
ManufacturerData
        CompanyId:0x0000(Ericsson Technology Licensing)
        Data:FF-FF-FF-FF-64-00-FF
ServiceUuids
        0000fff0-0000-1000-8000-00805f9b34fb
        0000ffb0-0000-1000-8000-00805f9b34fb
DataSections
        01(Flags):06
        02(Incomplete16bServices):F0-FF-B0-FF
        FF(Manufacturer):00-00-FF-FF-FF-FF-64-00-F
```

* アクティブスキャンのレスポンス
```
AdvertisementType:ScanResponse
Address:30E283XXXXXX Type:Public
LocalName:OKIN-BLE0008XXXX
Flags:
ManufacturerData
ServiceUuids
DataSections
        09(CompleteLocalName):4F-4B-49-4E-2D-42-4C-45-30-30-30-38-XX-XX-XX-XX
        12(Unknown):08-00-10-00
        0A(TxPower):04
```

## サービスとキャラクタリスティックの列挙

### サンプルコード C#
* [サンプルコード](https://github.com/HiroyukiSakoh/NerumAppBle/blob/main/BleExample/%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A8%E3%82%AD%E3%83%A3%E3%83%A9%E3%82%AF%E3%82%BF%E3%83%AA%E3%82%B9%E3%83%86%E3%82%A3%E3%83%83%E3%82%AF%E3%81%AE%E5%88%97%E6%8C%99%20.cs)
* ポイント
  * [BluetoothLEDevice](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice?view=winrt-22000)クラスの[FromBluetoothAddressAsync](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice.frombluetoothaddressasync?view=winrt-22000)メソッドを使ってインスタンスを取得
  * [BluetoothLEDevice.GetGattServicesAsync](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice.getgattservicesasync?view=winrt-22000)を使って GATTサービスを列挙する
    * [GattDeviceService.GetCharacteristicsAsync](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.genericattributeprofile.gattdeviceservice.getcharacteristicsasync?view=winrt-22000)を使って GATTキャラクタリスティックを列挙する
        * [GattCharacteristic.GetDescriptorsAsync](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.genericattributeprofile.gattcharacteristic.getdescriptorsasync?view=winrt-22000)を使ってGATTディスクリプタを列挙
        * [GattCharacteristic.ReadValueAsync](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.genericattributeprofile.gattcharacteristic.readvalueasync?view=winrt-22000)を使ってGATTキャラクタリスティックの値を取得

### サービスとキャラクタリスティックの一覧
サンプルコードで取得できた nerum appの一覧は以下の通り

|サービスUUID|キャラクタリスティックUUID|Properties|値|Descriptor|DescriptorValue|
|--|--|--|--|--|--|
|0x1800<br>(GenericAccess)|0x2A00<br>(DeviceName)|Read|"OKIN-BLE0008XXXX"|-|-|
||0x2A01<br>(Appearance)|Read|00-00|-|-|
||0x2A02<br>(PeripheralPrivacyFlag)|Read, Write|00|-|-|
||0x2A03<br>(ReconnectionAddress)|Write|-|-|-|
||0x2A04<br>(PeripheralPreferred<br>ConnectionParameters)|Read|50-00-A0-00-00-00-E8-03|-|-|
|0x1801<br>(GenericAttribute)|0x2A05<br>(ServiceChanged)|Indicate|-|※2|02-00|
|0x180A<br>(DeviceInformation)|0x2A23<br>(SystemId)|Read|D4-46-83-00-00-83-E2-30|-|-|
||0x2A24<br>(ModelNumberString)|Read|"GCBT40-1S"|-|-|
||0x2A28<br>(SoftwareRevisionString)|Read|"V1.2.5"|-|-|
||0x2A29<br>(ManufacturerNameString)|Read|"BLE-4.0 Module"|-|-|
|0xFFE0|0xFFE4|Notify|-|※2|00-00|
|||Notify|-|※1|UART Channel|
|0xFFE5|0xFFE9|Write|-|※1|BLE Channel|
|0xFF20|0xFF21|Write|-|※1|Link Interval|
||0xFF22|Write|-|※1|Modify Name|
||0xFF23|Write|-|※1|Modify Uart Rate|
||0xFF24|Write|-|※1|Modify RF Power|
||0xFF25|Write|-|※1|Sleep Enable|
||0xFF26|Write|-|※1|Modify Advert Time|
||0xFF2F|Notify|-|※2|00-00|
|||Notify|-|※1|Config Notify|
|0xFF30|0xFF31|Read, Write|"00000"|※1|PWM0|
||0xFF32|Read, Write|"00000"|※1|PWM1|
||0xFF33|Read, Write|"00000"|※1|PWM2|
||0xFF34|Read, Write|"00000"|※1|PWM3|
||0xFF35|Write|-|※1|PHASE|
||0xFF36|Read, Write|"65534"|※1|FREQUECY|
||0xFF37|WriteWithoutResponse|-|※1|PWM|
||0xFF3F|Notify|-|※2|00-00|
|||Notify|-|※1|PWM Status|
|0xFF40|0xFF41|Read|01-62|※1|ADC0|
||0xFF42|Read|01-62|※1|ADC1|
|0xFF50|0xFF51|Write|-|※1|GIPO Setting|
||0xFF52|Write|-|※1|GPIO Mode Setting|
||0xFF53|Read|"111111"|※1|Read GPIO Status|
||0xFF54|Read, Write|FF-FF-FF-FF-FF-FF-FF-FF-FF-<br>FF-FF-FF-FF-FF-FF-FF-FF-FF|※1|Data Backup|
||0xFF5F|Notify|-|※2|00-00|
|||Notify|-|※1|GPIO Notify|


Descriptor
※1 0x2901(CharacteristicUserDescription)
※2 0x2902(ClientCharacteristicConfiguration)

GCBT40というモジュールを使っているらしい
[マニュアルっぽいのを見つけて](https://www.silicontra.com/UploadFile/Soft/GCBT40.pdf)、DeepLで翻訳してみたが、
一部サービスUUIDが異なるなど、リビジョンが違う模様。

# BLEスニッファ

GATTキャラクタリスティックだけではどうやって操作するかわからないので、
実際のスマホアプリとベッドの通信を覗き見て、理解を進める。

## セットアップ

公式のセットアップだけ見ておけばOK

* 公式のセットアップ
  [Using with Sniffer V2 and Python3 | Introducing the Adafruit Bluefruit LE Sniffer | Adafruit Learning System](https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-sniffer/using-with-sniffer-v2-and-python3)
* 公式のFAQ
  [FAQs | Introducing the Adafruit Bluefruit LE Sniffer | Adafruit Learning System](https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-sniffer/faqs)
* そのほか参考にしたサイト
  [Bluefruit LE Snifferを使用したBLEプロトコル解析 | TomoSoft](https://tomosoft.jp/design/?p=41647)
  [Big SurでAdafruit Bluefruit LE Snifferを使う手順](https://zenn.dev/nnabeyang/articles/ad71028a39ad78)

## キャプチャ

1. WireSharkを起動し、`nRF Sniffer for Bluetooth LE`を選択する
    <img width="600" alt="WireShark1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/3586c327-6eb6-773d-8110-00101c7e1a4c.png">
1. アドバタイズが表示される
    <img width="600" alt="WireShark1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/d9056f8f-160f-e620-1318-5a2afe7ca32f.png">
1. 目的のデバイスを選択する
    <img width="600" alt="WireShark1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/2ca241ab-61b5-b910-b598-1882a7558b27.png">
1. スマホアプリからBLEデバイスに接続を開始し、 Master/Slaveの組み合わせが出るのを待機する
  出てこない場合、スマホアプリで切断⇒接続を繰り返すこと
  (アドバタイジングは3つの周波数で行われるので、2/3の確率で別のチャンネルを見ているタイミングで接続を開始したことによるもの。)
    <img width="600" alt="WireShark1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/dba2a1ce-57aa-8dcf-6aa3-58e6321d4f74.png">
1. ここまで来たら後は 各GATTとどういうやり取りをしているか、
  アプリを動作させつつ観察していくだけです。
    <img width="600" alt="WireShark1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/55a9124c-8501-6f60-a3d3-ea680293eb74.png">
  フィルタに`btatt`を指定するとGATT通信のみに絞れます
  他には、`btatt.opcode == 0x12`で送った値だけに絞れたり
  `btatt.opcode == 0x1b`で通知された値だけに絞れたり

# nerum appのプロトコル解析
## 通知
スマホアプリでは、以下のキャラクタリスティックからのみ、通知を受けている
サービスUUID:`0xFFE0`、キャラクタリスティックUUID:`0xFFE4`(Description:`UART Channel`)
```
ServiceUuid 0000ffe0-0000-1000-8000-00805f9b34fb
CharacteristicsUuid 0000ffe4-0000-1000-8000-00805f9b34fb
```


アプリには大きく2画面ある（操作とタイマー設定）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/9b2a918a-4d5e-c5f0-661b-71ee7359b4ff.png)

* 操作側を表示中、次のようなNotificationが1秒毎に送信されている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/504d6dd6-23a6-f841-c04b-8f3305b86413.png)

  いろいろと操作・観察した結果以下のような構成であることが分かった
  * ペイロードは19または20バイト区切りで2つの通知に分割されている
    (大抵は合わせて23バイト たまに分割の境界が1バイト分消えることがあった よくわからん)

  |長さ|意味|値の例|説明|
  |--:|--|--|--|
  |3|マジックワード|F4FE16||
  |2|HEAD MOTORの開度|0000|0x0000~0x3338の間<br>最大値多少の変動あり|
  |2|FOOT MOTORの開度|0000|0x0000~0x242Eの間<br>最大値多少の変動あり|
  |12|不明(固定)|0000000010000000000000||
  |1|照明|40|ON:0x40<br>OFF:0x00|
  |3|不明(固定)|FF0001||
  |1|チェックサム|A7|全バイトの合計の下位1バイトをビット反転した値<br>例：合計 10進数`856`=`0x0358`<br>`0x58(0b0101100)`のビット反転が`0xA7(0b10100111)`|

* タイマー設定側を表示中、次のようなNotificationが1秒毎に送信されている
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/eddd7895-e9e4-962b-4419-84d26704572d.png)
  いろいろと操作・観察した結果以下のような構成であることが分かった

  |長さ|意味|値の例|説明|
  |--:|--|--|--|
  |3|マジックワード|ED8003||
  |1|アラーム時間(HH)|0F|0x00～0x18の24h|
  |1|アラーム分(MM)|32|0x00～0x3bの60m|
  |1|繰返(曜日)|82|最上位ビットから順に<br>ON/OFF,土曜日,金曜日,木曜日,水曜日,火曜日,月曜日,日曜日<br>ONかつ土曜日のみ場合 0b11000000⇒0xC0|
  |1|モード|12|なし = 0x00<br>M1 = 0x11<br>M2 = 0x13<br>灯り = 0x12|
  |8|不明(固定)|0000000000000000||
  |1|チェックサム|BA|(操作側と同じ)|


## 操作
スマホアプリでは、以下のキャラクタリスティックにのみ、書き込みを行っている
サービスUUID:`0xFFE5`、キャラクタリスティックUUID:`0xFFE9`(Description:`BLE Channel`)
```
ServiceUuid 0000ffe5-0000-1000-8000-00805f9b34fb
CharacteristicsUuid 0000ffe9-0000-1000-8000-00805f9b34fb
```

### ベッドの操作
いろいろと操作・観察した結果以下のような構成であることが分かった
|長さ|意味|値の例|説明|
|--:|--|--|--|
|3|マジックワード|E6FE16||
|5|コマンド|01000000|詳細は次の表へ|
|1|チェックサム|04|(通知と同じ)|

|操作|マジック|コマンド部|チェックサム|メモ|
|--|--|--|--|--|
|HEAD-UP|E6FE16|0100000000|04||
|HEAD-DOWN|E6FE16|0200000000|03||
|FOOT-UP|E6FE16|0400000000|01||
|FOOT-DOWN|E6FE16|0800000000|FD||
|HEAD+FOOT-UP|E6FE16|0500000000|00|(おそらく、1バイト目の下位4ビットの組み合わせ)|
|HEAD+FOOT-DOWN|E6FE16|0A00000000|FB|同上|
|メモリ(FLAT)|E6FE16|0000000800|FD||
|メモリ(ANTISNORE)|E6FE16|0080000000|85||
|メモリ(不明)|E6FE16|0040000000|C5|試しにやってみたら、特定の姿勢になった|
|メモリ(READING)|E6FE16|0020000000|E5||
|メモリ(ZG)|E6FE16|0010000000|F5||
|メモリ(M1)|E6FE16|0000010000|04||
|照明ON&#124;OFF|E6FE16|0000020000|03||
|メモリ(M2)|E6FE16|0000040000|01||
|停止コマンド？|E6FE16|0000000000|05|FLAT等メモリの状態へ姿勢変化中に送信すると、姿勢変化が停止する|
|状態通知開始コマンド？|E6FE16|0300000000|02|タイマー画面から操作画面へ切り替えた際に、このコマンド送信している。<br>このコマンド後、F4FE16のNotificationが飛ぶようになる模様(ほかのE6FE16でも同様)<br>1バイト目の0x03の意味は分かっていない|

### タイマー設定
* タイマー画面に切り替えたタイミング
以下のコマンドを送信している
`E28003009A`
このコマンド後、ED8003のNotificationが飛ぶようになる

* ON/OFF操作時に以下2個のコマンドを送信している
1.タイマー設定側を表示中のNotificationと同じコマンド
2.時間で変化するコマンド(タイマーの起点となるシステム時刻設定をしている？）

  |長さ|意味|値の例|説明|
  |--:|--|--|--|
  |3|マジックワード|E78001||
  |1|年|7A|1900年を0とする値<br>例:7a=122=2022年|
  |1|月|05|1月を0とする値<br>例:05=6月|
  |1|日|07|1日は1とする値<br>例:07=7日|
  |1|時|00|例：0時|
  |1|分|0D|例：13分|
  |1|秒|38|例：56秒|
  |1|チェックサム|CC|(通知と同じ)|

# 試作アプリの実装
解析したコマンド類の読み書きで実際に操作できるかアプリを試作した。
* [サンプルコード](https://github.com/HiroyukiSakoh/NerumAppBle)
* 試作アプリ
  * 指定したBluetoothAddressに接続
      ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/2c0351e8-da66-fb8a-5d97-e4dec80f06e7.png)
  * 任意の操作と状態の表示
      ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/64b280da-cdca-e301-d859-176db7a81d53.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/fd4c2d58-e29f-728a-b773-49b1cd6cd745.png)
  * Debug用 Notification/Writeを出力
      ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260166/6b2d8fec-b8bb-bf8c-e9d3-3ecda8d87972.png)


# 今後
* 操作プロトコルをもっと理解したい（何か隠された機能とかないかな とか
  * GooglePlayストアのアプリを逆コンパイルすればいろいろ分かりそう
* スケジュールのダイナミックな操作、オートメーション化
  * パラマウントベッド ActiveSleepBedのような入眠補助
  * 土日と平日で別スケジュールの実現
* BLEのスタック周りWindowsから脱出したい、RasPi(Linux上)で動かしたい