半年ほど前に買ったエアロバイク
しかし半年で5回ほどしか使ってないという残念な状態のため、エンジニアリングを持って供養したいと思います。
エアロバイクについて
上記のエアロバイクにはTuya Smartというメーカーから出してるIoT製品全般のアプリがあります。エアロバイクとアプリはBluetoothで接続される仕組みです。こちらのアプリは使い勝手が微妙に良くなかったり日本語訳がちょっと怪しかったします。
そこで今回は自作iOSアプリからBLEで直接繋げて情報を取得できないか?というのを試してみます。公式でアナウンスがあったり想定してる使い方ではないので動作の結果は自己責任となりますのでご了承ください。ちなみにエアロバイク側がPeripheral、専用アプリ・自作アプリがCentralになります。BLE関連の用語・言い回しで誤りがあったらすみません。
用意するもの
・Mac
・iPhone
→後述しますがMacにはXCode、PacketLoggerを使用します。iPhoneはTuya SmartとLightBlueをインストールします。
接続情報を調べる
後方に以下のようなBluetoothモジュールが隠されています。引っ張ったら出てきたので単4電池2つセットします。
次に少し自転車をこぎます。すると電池カバーのライトが赤く点滅します。すかさず専用アプリからエアロバイクを接続しましょう。足を休めるとすぐに電源がオフになるストイック仕様なのでこぎ続けます。
接続完了すると今度はiPhoneの[設定]アプリからBluetoothを開きます。これはiPhoneの仕様なのか何なのか分からんのですが、iPhoneは一度デバイスを接続するとデバイスのLocal Nameで表示されるようになります。
エアロバイクのLocal NameはTYということがわかりました( ✌︎'ω')✌︎
次にこのエアロバイクを専用アプリ以外からScanしてみます。LightBlueというアプリが分かりやすくておすすめです。LightBlueを起動すると近くのデバイスの一覧の中にTYというデバイスが表示されます。
↑もし一度もアプリを接続せずにScanをするとTYという表示はされないはずなので、先ほど一度専用アプリで接続したことには意味がありました。ちなみに上の写真のTYがグレーになっていて接続することができません。これは専用アプリの方でBLE接続しているせいなので、専用アプリの方を落としてLightBlueをリロードすると接続できるようになります。
接続すると↑のようにデバイスのUUIDとNotifyやWriteの2つのサービスがあることが分かりました。重要なのはUUIDと2つのCharacteristicのUUIDです。次は実際にSwiftで書いて接続を試みます。
Swiftの実装
iOSの新規プロジェクトを作成してInfo.plistにBluetoothの権限を追加します。
Privacy - Bluetooth Always Usage Description
実装は先ほどのLightBlueを見る限りNotifyを使ってエアロバイク→専用アプリへ連携していると考えられます。そこでBLEをScan、Connect、Notifyの3つを行ったのが以下のコードになります。StoryBoardと違ってUIも付いてるのでペライチのコピペで済みます。
しかし上記では自転車をいくらこいでもNotifyが呼ばれませんでした。嫌な予感はしてましたが、どうやらNotifyを受信するためにWrite Without Responseでアプリ側から何かしらのデータを送らないといけないようです😥
BLEパケットキャプチャをインストール
専用アプリ→エアロバイクへの通信仕様なんて分からないので詰んだ...と思いきやiPhoneのBluetooth通信をキャプチャできるツールがあると知りました。AppleDeveloperにこんなのがあったとは。qiita.com
上記を参考にMacとiPhoneにそれぞれPacketLoggerとプロファイルをインストールします。PacketLoggerを起動したらFile→New iOS Traceを選択し、Packet Type FilterをATT ReceiveとATT Sendにします。
ログの出し方はやや複雑です。iPhoneをMacに接続した状態でエアロバイクをこいで電源がONになったら専用アプリを起動します。そしてPacketLoggerの画面を見ると以下のような”接続してるっぽい”ログが出ました。
↑先ほどLightBlueで確認した2B10(Notify)と2B11(Write Without Response)のキーワードが表示されていますね。15行目の2B10(Notify)がSendしてるのは何故なのかよく分からないのですが、気になるのは21〜27行目のNotifyをReceiveしているところです。つまり21行目より前のWriteが専用アプリ→エアロバイクに対して"これから走るぞー"みたいなデータを送っていると推測できます。
そこで直前の2B11(Write Without Response)の19,20行目に注目します。こちらの2行のValueが固定の値なのか一応何度かログを取ってみました。
# 1回目
0021 3004 4A4C 4337 5351 3047 4730 4551 4748 3051
0111 F8F7 66C8 F43E EABE D131 ED44 C9A1 9C
# 2回目
0021 3004 3735 424F 3635 5133 3337 4E4F 4131 534C
0136 1957 3FA3 EE19 E3CD A257 8757 0D97 9D
# 3回目
0021 3004 3444 4E51 3937 3532 4147 3241 4B51 5134
01A7 B083 560E 9C6D 1ED9 11E1 5C79 8A73 49
先頭の0021 3004が共通点ですね。他の接続毎に異なるデータはタイムスタンプなのか接続回数なのかランダム値なのかは分かりませんが、そこまで厳密な制御は入ってないだろうと期待して3回目のデータ2行を自作アプリのWrite Without Responseで送信する処理を加えます。
ここまでのフローを整理すると
- BLEデバイスをスキャン
- Local NameがTYである、又はペリフェラルのUUIDがLightBlueのアプリで確認したUUIDと等しいという条件でフィルター
- 2と一致した場合はスキャンを停止
- 2に接続(connect)
- connectされたペリフェラルからServiceの一覧を取得
- ServiceからNotifyとWrite Without ResponseのCharacteristicを取得
- Notifyを開始
- Write Without Responseで前述のデータを送信
という手順で実装しました。
8 . Write Without Responseで前述のデータを送信をやってみたところ、1回のWriteに対してNotifyが7回Receiveしました。こちらはPacketLoggerと同じ挙動なのでここまではうまく出来てそうです。
しかしその後自転車をこいでもNotifyは呼ばれないので、おそらく定期的(あるいはNotifyを全て受け取った後?)にもう一度Write Without Responseを実行する必要がありそうです。これはさっきのPacketLoggerにも交互にログが出ていたので間違いなさそうです。
長くなったので一度まとめます。
残りの課題は2回目以降のNotifyを受け取るにはどうするか、受け取ったデータ(バイナリ)がどういう意味を持つのかを検証することです。後者のデータの中身についてはアプリ側の画面の項目から消費カロリー、走行距離、負荷(G)、ケイデンス(rpm)は含まれているはずです。そのうち負荷や走行距離はコントロールしやすい値なのでバイト列を追っていけば推測できると思います。総時間や総走行距離、総消費カロリーといった過去の値を合算したものはエアロバイク側で持つと冗長なのでアプリ側で出してる気がします。
続きはまたそのうち。。最近忙しいのであまり時間取れず。