自分の生死状態をリアルタイム公開したい！

> 本記事は [mast Advent Calendar 2023](https://adventar.org/calendars/9098) の 10 日目の記事です。9 日目の記事は Mahi さんによる「[AirPodsのケースを電車内に落とした話](https://note.com/mahikun/n/nb3acf50892ca)」でした。僕も最近ワイヤレスイヤホンを導入したので、電車内に置き忘れないよう気をつけたいところです。

:::note info
技術系の記事ですが、技術に詳しくない方でも楽しく読めるように構成しています。ぜひ最後までお読みいただけると幸いです！
:::

\***

12 月もいよいよ 3 週目に入り、毛布を出し始める方も多い頃ではないでしょうか。僕はもうこたつも毛布も出しています。

さて、**人間は明日ぽっくり逝く**かもしれません。しかし人間が死亡したとき、 その人のネット上の友人たちは、どのようにしてそれを知ることができるのでしょうか。僕は親に X のパスワードを教えていないので、

> 「ちゅるりの母です。ちゅるりは…」

とツイートしてもらうことができません（*パスワードを親に教えて生じる悲劇については、あなたの[ブックマーク欄](https://twitter.com/i/bookmarks)や別の閲覧用アカウントなどをご参照ください*）。
従って、**自分で出したケツは最後まで自分で拭ききる必要**が生じます。今回はそのおしりふきとして、**自分の生死状態を常に Web 上に公開するシステム**を構築したのでご紹介します。

# 作り方の検討
今回のシステムで重要となってくるのは、**何をもって死とするか**でしょう。死の指標はたくさんありますが（また定義もたくさんある）、例えば、次のような指標が思い浮かびます：

- ツイートが n 日間無くなった
- 位置情報が動かなくなった
- 新聞の訃報欄に載った
- 心臓が止まった

それぞれ検討してみましょう。

- **ツイートが n 日間無くなった**
この場合、ネット活動における死はこれで感知できますが、イコール生物的な死ではありません。ネット活動を停止した人の親が、「〇〇の母です。…」とツイートすることはまずないでしょう。

- **位置情報が動かなくなった**
一見良さそうですが、人間はいつだって位置情報を計測できるわけではありません。また、**位置情報の計測を切りたいときだってあり**ます。よって却下。

- **新聞の訃報欄に載った**
これもぱっと見良さそうですが、同じ地域に住む同姓同名の人が亡くなったらどうでしょうか。自分が死んでいないのに、死亡報告がなされてしまう危険があります。また、そもそも**訃報欄に載るかどうかすら分かりません。**

- **心臓が止まった**
多くの人間は意図して心臓を止めることはできないので、**死の指標としてはこれが良さそう**です。また、近年はさまざまな手法で心拍を測ることができるので、技術的にそこまで難しくなさそうに見えます。

というわけで、**心拍数計測に決まりました！** :tada:
ですが、計測方法はどうすればよいでしょうか。少し考えてみると、いくつかの手法が思い浮かびます：

- センサ等を買ってきて計測器を自作する
- 外側ライトを付けた状態のスマホのカメラに指を押し当て、各フレームごとの中心輝度を分析する
- スマートウォッチをどうにかする

僕にはお金も技術がないので、計測器を作ることはできません。また、スマホを使った手法を一度試してみましたが、極値計算等が面倒くさかった[^11]のでやめました。従って、今回は**スマートウォッチで計測**を試みてみます。

# 謎中華スマートウォッチ
幸いなことに、手元に**謎の中華スマートウォッチ**がありました。おそらく高校のときに買ったものでしょうが、殆ど使ったことがありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/d03bb13f-93b7-5af8-331f-e0c4584f4df0.png "謎中華スマートウォッチ")

従って、**型番もモデルもわからない**のでどうしようもありません...が、**どうしようもないものをどうにかしたくなるのがエンジニア**です。ここで僕は燃えてしまいました。このスマートウォッチからどうにかして心拍数を取ってくるのです。

\***

少し触ってみると QR コードが出てきました。どうやらこの QR コードは **F fit** というアプリを示しているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/35f4a3cc-9b7b-8388-2810-41fa68312a84.png)

図は Android 用ですが、iOS 用のアプリもあるようなので自分の MacBook にインストールしてみます。

:::note warn
意味不明な中華アプリをインストールすることは、本来であれば推奨されません。インストールする際は自己責任で行ってください。
:::

早速アプリを起動してみます。適当に接続して設定を済ませれば、下のような画面が出てきます。

<img width="256" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/ffefbd54-0439-de75-0539-06380a18a429.png" />

赤枠で示したリロードボタンを押すと、見事アプリに計測データが飛んできました！（数日つけて事前に計測を行っています。）つまり、**ボタンを押したときに何が行われているかが分かれば、心拍数を取得できる**ことが分かります。

# 通信を解析する
この手のスマートデバイスは、たいてい **BLE（Bluetooth Low Energy）** と呼ばれる規格で通信がなされています。雑に言えば Bluetooth です。この予想を確かめるために、まずは Apple 謹製の [PacketLogger](https://developer.apple.com/download/more/) をインストールします。

PacketLogger を起動した状態でさっきのリロード操作を行ってみます。そのときのログが以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/c167510f-030b-3be6-4dc5-47bce8f19b0c.png)

やはり、アプリとスマートウォッチは BLE で通信を行っているようです:tada:　このログによれば、通信は

1. リロードボタンを押す
2. アプリからスマートウォッチへ、データ[^1] `AB0000090000000001000100045F0F67` を送信する(4 行目)
3. アプリからスマートウォッチへ、データ[^1] `AB00000600000000010057000101` を送信する（7 行目）
4. スマートウォッチから計測データと思われるものが `Handle Value Notification` として大量に送られてくる（10 行目以降）
5. アプリに計測データが反映される

といった手順が踏まれているようです。何度か試してみましたが、2 と 3 で送信されるデータはこの値しか観測されませんでした。

しかし、この**計測データと思しきものがどのようなデータ形式であるかが分からない**ので、ここから心拍数データを持ってくる術がありません...。

![アートボード 1@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/2ab83de7-4425-f81e-f489-3a69f4774222.png)

# アプリの解析
ここで僕は、アプリを解析することを思いつきました。計測データがスマートウォッチから飛んできたとき、アプリの内部ではどのような処理がなされているのかを見てみるのです。

幸いなことに、F fit は Android 向けアプリを公開しています。Android アプリは通常、Java や Kotlin といった言語で開発されているので、**ツールを用いれば逆コンパイル[^2]をすることが可能**です。

\***

まず、怪しいサイトから F fit の apk 形式[^3]のアプリケーションをダウンロードして Android アプリを手元に持ってきます。続いて、**jad** と呼ばれるツールを用いて apk 形式の Android アプリを Java プログラム（＋その他画像ファイルなどのリソース）群に変換します（参考：[【Android】APKをデコンパイルしてみる - Qiita](https://qiita.com/ist-m-k/items/904e842c691e4a66e4a8)）。

:::note warn
再三述べていますが、怪しいサイトからダウンロードしたものは極めて怪レいので、へんなデータやウイルスが紛れ込んでいることがあります。筆者は、過去に何度も使用しているサイトからダウンロードしているので一定の信頼をおいていますが、本来は安易にダウンロードすべきではありません。
:::

jad を使用して展開した F fit アプリには、次の画像（左側）でわかるように大量のプログラムやディレクトリ[^6]があります。これでは**どれを読んで良いのか分かりません**。これはバンドル[^4]されたライブラリ[^5]のコードが大量に含まれているためで、実際に開発者が書いたプログラムはほんの一部です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/8eb894dc-68e2-e421-f1c6-cc5f05e76ba3.png)

ところで、Google Play でアプリのリンクを共有すると `https://play.google.com/store/apps/details?id=com.twitter.android` のようになります。特にこの中の `com.twitter.android` はアプリの ID を表しています。慣例的に**このアプリ ID と自分のプログラムのパッケージ名[^7]は一致させる**ので、この例のアプリ（Twitter）の場合プログラムは `.../com/twitter/android` の下にあることが分かります。

このことから、F fit の Google Play における共有リンクを取ると **`com.zjw.ffit` がパッケージ名であることが分かります。** すべての Java プログラムは `src/main/java` 以下に配置されるので、これと結合した **`src/main/java/com/zjw/ffit` 以下のプログラムが F fit に関連したプログラムである**ことが判明しました！

\***

あとは根気です。このディレクトリの下でそれっぽいコードを検索し、あるコード片に突き当たりました[^10]。このコードによれば、計測データは次のようになっているようです（計測データをバイト配列 `bArr` とし、この配列の n 番目のデータを `bArr[n]` と表します）：

- `bArr[0]` が `-85`、`bArr[8]` が `3` であるとき、なんらかの計測データである
- 上に加え、`bArr[10]` が `13` であるとき、**心拍データ**である
- 心拍数データについて
    - `bArr[17]` から `bArr[294]` までが **5 分おきに計測した心拍数である**
    - 例えば、`bArr[17]` は 0 時 00 分、`bArr[18]` が 0 時 05 分 といった具合である

もうこれで完全に計測データを理解したことになりますね。驚くべきことに、**暗号化などは一切行われていませんでした**。

![アートボード 1@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/2edb798e-3017-1f54-9859-e55df76184d5.png)

# 心拍数を取るコードを書く
以上のリバースエンジニアリング[^9]により、謎中華スマートウォッチから心拍データを引き抜く方法が判明しました。これらの情報をもとに、Python を用いて心拍データ引き抜きプログラムを書いてみます。

なお、プログラムを書くに当たり [WT901BLECLにBLE接続してデータを読み取る - Zenn](https://zenn.dev/fastriver/articles/wt901blecl_read_data) を参考にしています。

<details><summary>プログラムはこちら</summary>

```python
import asyncio
from bleak import BleakClient

# リバースエンジニアリングしてデバイスの UUID も引き抜いておいた
mac_address = "04012706-5236-286E-A505-B795514E831D"
write_uuid = '00000002-0000-1000-8000-00805f9b34fb'
read_uuid = '0000ffe0-0000-1000-8000-00805f9b34fb'
notify_uuid = '00000003-0000-1000-8000-00805f9b34fb'

heart_flag = False
exit_flag = False
buffer = []


def notification_handler(sender, data: bytearray):
    global heart_flag
    global exit_flag
    global buffer

    if len(data) >= 10 and data[0] == 0xab and data[8] == 0x03 and data[10] == 0x0d:
        buffer.extend(data[17:])
        heart_flag = True
    elif heart_flag and len(data) != 5:
        buffer.extend(data)
    elif heart_flag and len(data) == 5:
        buffer.extend(data)
        heart_flag = False
        exit_flag = True


async def run(address, loop):
    global exit_flag
    async with BleakClient(address, loop=loop) as client:
        x = client.is_connected
        print("Connected: {0}".format(x))
        await client.start_notify(notify_uuid, notification_handler)

        await client.write_gatt_char(write_uuid, bytes.fromhex("AB0000090000000001000100045F0F67"))
        print("wrote phase1")
        await client.read_gatt_char(read_uuid)

        await client.write_gatt_char(write_uuid, bytes.fromhex("AB00000600000000010057000101"))
        print("wrote phase2")
        await client.read_gatt_char(read_uuid)

        while not exit_flag:
            await asyncio.sleep(1)


loop = asyncio.get_event_loop()
loop.run_until_complete(run(mac_address, loop))

print(buffer)
print(len(buffer))
```
</details>

# システムを作る
あとは、数十分おきに心拍データを読み取り、サーバ上にアップロード・表示するシステムを作るだけです。特に特筆すべきことはなく CloudFlare 上にデプロイし、データ保存を D1、API を Workers で構築します。フロントエンドは Pages 上に https://itsu.dev を持っているので、そこの 1 ページとして生やします。

![アートボード 1@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/6c08d651-53d1-1313-bc91-0cb0981f87a0.png)

最終的に **https://itsu.dev/osirifuki で生死状態を公開**しました。生きていれば「生きています」、死んでいれば下の画像のように「死亡しています」と表示されます。また、直近の心拍数遷移もグラフで表示されるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237903/5364de1c-2471-118f-48ad-49fb2ae95c8b.png)

**これにて目標は達成です！！** わいわい :tada:

**2023/12/10 0:25 追記**
次のようなご質問をいただきました。
> あと最後に公開サイトに飛んで生存確認できるのもいいね  あれって通信途絶or外してる時は死亡判定になるんですか？（[X より](https://x.com/Units_SI/status/1733506816792039466?s=20) ）

直近の計測で心拍が 0 であれば死亡判定するようになっています。
つまり、「**スマートウォッチを外すと死亡する**」ということですね。

# おわりに
本アドカレに向けて、2週間ほど前から謎中華スマートウォッチを解析しつつ、自分の生死状態をリアルタイムで公開するようなシステムを構築してみました。この開発を通して謎中華スマートウォッチの通信について詳しく慣れたのは良かったです（！？）。今回は心拍数のデータのみで遊んでみましたが、他にも歩数計や血圧計のデータも取れそうだったので、今後はそのあたりで遊んでみたいと思います。

いつもは Web を中心にプログラミングをしていますが、Web をしているとどうしても他の分野のキャッチアップが億劫になってしまいます。今回アドカレという機会を通して、別の分野も積極的に触ることができて大変ためになりました。プログラミングは面白くないと言われがちですが、このようにしておもしろいこともできます。確かに授業のプログラミングだけをしていると、物足りなさやつまらなさを感じてしまう方もいるかもしれません。しかしこうして、創作や本当に困っている物事への手段としてプログラミングを活用していけば、きっと楽しいものとなるはずです。ぜひたくさん手を動かして、のめりこんでいきましょう :tada:

[^1]: このデータは 16 進数のバイト配列です。例えば Python では `[0xAB, 0x00, ... , 0x67]` と表されます。
[^2]: コンパイルとは、人間様が読めるプログラムを機械が読めるような形式に変換することを指します。逆コンパイルではこの逆のことで、機械用の状態になっているソフトウェアを人間様が読めるプログラムに変換することを言います。
[^3]: 通常、Android アプリをダウンロードする際には Google Play などのサイトを使用しますが、
これを使用するとアプリのデータをダウンロードすることはできず、端末に直接インストールされてしまいます。これを回避するために apk 形式（Android アプリの形式。zip ファイルとほぼ変わらない）でアプリを頒布している怪しいサイトを使用します。
[^4]: ライブラリ（注釈参照）をアプリに含めること
[^5]: ソフトウェア開発では、他の人が書いたプログラムや便利なプログラムを自分のプログラムに取り込む、という概念があります。これをライブラリと呼びます。いわばプログラムの部品のようなものです。
[^6]: フォルダのこと。
[^7]: フォルダ・パスのこと。
[^8]: Java におけるデータ構造の一つで、16 進数で表現された数字を順番に連ねて表現したもの。
[^9]: 仕様書などがない状態で製品の仕組みや仕様を分析すること。
[^10]: 謎中華アプリとはいえ、著作権の問題もありそうなので公開は控えています。
[^11]: スマホの懐中電灯をつけたままの状態でカメラを起動し、指をカメラに押し当てます。すると、脈動に合わせて画面の明るさが若干変わるので、それを利用して心拍数を計測する手法があります。技術検証中にこのような動作をする Android アプリを途中まで作ってみましたが、輝度の極値計算で躓いて諦めました。