Slack BOT から新型ルンバ980をWiFiで制御する【後編】

Date
December 5, 2015
image

はじめに

おうちハック Advent Calendar 2015の5日目の記事です。

こんにちは。CTOのkamocです。 前回の記事(ルンバがうちに、やってきた。)では最新モデルのルンバ980をハックしていく方向性について書きました。 今回はアプリとiRobot API間の通信をハックして行きます。

最初に

今回は図の★の部分。アプリとiRobot API間の通信をハックして行きます。

image

今回のゴール

今回の記事では、ルンバに「掃除しろ」と命令したらルンバが掃除を開始する Slack BOT を開発します。

iRobot アプリの掃除開始命令を解析し、Slack BOT から同命令を叩いてルンバに掃除させます。

通信内容を観察してみる

それでは早速 iRobot アプリと iRobot API の通信を解析してみましょう。 通信の解析には Charles というツールを使用しました。

iPhone のパケットキャプチャのやり方はこちらを参考にしました。

iPhoneのパケットキャプチャ - Qiita

Mac(本投稿ではOS X 10.9〜で説明)でのiPhoneのパケットキャプチャについて投稿します。 色々なやり方があると思いますが、自分は ・HTTPリクエストとレスポンス内容を確認する場合はCharles。 ・ネットワーク関連のエラー解析を行う場合はWireShark を使っております。 上記2つについて手順や自分が思う良いところ(個人的な見解ですが。。)などついて記載します。 HTTPプロキシとしてPC上で動作し、iPhoneからのリクエストとレスポンスをモニタリングします。 参考: http://www.charlesproxy.com/ 1.下記Webページの"Download a free trial"からCharlesをダウンロード/インストール。 http://www.charlesproxy.com/download/ 2.MacをWifiに接続。 3.Charlesのメインメニュー"Proxy"→"Proxy Settings..."を開いて下記を設定。   ・Proxies タブでPortを8888 にセット。   ・SSL タブで"Enable SSL Proxying"をチェック。   ・Locationsに*を追加。 1.設定→Wi-FiでMac側と同じWifiに接続。 2.接続したWifiの( i )ボタンをタップ。 3.下側にスクロールして、HTTPプロキシ「手動」を選択後、下記をセット。   ・サーバ:MacのIPアドレス(Mac側で設定→ネットワークで確認)   ・ポート:8888   設定後、Charles上でiPhone側のHTTPパケットが見える。 4.上記HTTPプロキシ設定をした状態で、下記リンクからCharlesのSSL証明書をダウンロード&インストール。 http://charlesproxy.com/getssl        ↓ 設定後、Charles上でiPhone側のHTTPSパケットが見える。 HTTPのリクエスト/レスポンス内容が確認しやすい。 HTTPSのリクエスト/レスポンス内容も確認できる(自分は特にこれが素晴らしいと思っています)。 3G,LTEなどの電話回線でのキャプチャが出来ない。Wifi でのみキャプチャ可能(HTTPプロキシなので)。 HTTP/HTTPS以外がキャプチャできない。 ネットワークエラー発生時に原因がよくわからないがある("504 Gateway timeout"発生時に"Broken Pipe"になってることがあった)。 無料版(トライアル版)はアプリ起動後30分で終了する。正式版は有料($50)。 iPhone側のネットワークデバイスを仮想インターフェイスとしてMac側にマウントしてパケットキャプチャする方法。 参考: http://qiita.com/ionis_h/items/661a9a9d41cb1574e357 http://qiita.com/isaoshimizu/items/da3e74d5a8e9b1d6b6a4 1.下記からWireSharkをダウンロード/インストール。   XQuartz(WireSharkで使用)も必要になるのでこれもダウンロード/インストール。 http://www.wireshark.org/download.html https://xquartz.macosforge.org/landing/ 2.MacにiPhoneをUSB接続。 3.ターミナルで下記コマンドを実行(iPhone側のネットワークをMACにアタッチ、キャプチャ開始)。 $rvictl -s iPhoneのudid(オーガナイザに表示されるiPhoneのidentifer) $tcpdump -i rvi0 -w [ダンプファイル名] 4.iPhoneで適当に通信を伴う操作(アプリでwebページを開くなど)をする。 5.ターミナルで下記コマンドを実行(キャプチャ終了)。 $rvictl -x iPhoneのudid(オーガナイザに表示されるiPhoneのidentifer) →これでターミナルのカレントフォルダにダンプファイルが作成される。 6.WireSharkを起動。   ※たまにWireSharkが起動しないときがありますが、Macへ再ログインすると解消する場合があります。 7.WireShark上でメインメニュー"File"→"Open..."を選択。 8.tcpdumpで作成したダンプファイルを選択。 ...

iPhoneのパケットキャプチャ - Qiita

Charles を起動して iPhone のプロキシ設定を済ませた状態で、iRobotアプリを立ち上げるとこんな感じになります。

image

いろいろ通信してますね〜。 http://www.irobot.com と https://irobot.axeda.com のどちらかが iRobot API でしょう。この後詳しく見ていきます。 https://e.crashlytics.com はアプリのクラッシュレポートや利用状況を記録しているやつですね。いつもお世話になっています。 http://192.168.10.4 はルンバとの直接通信ですね。ルンバのIPアドレスは前回の記事で書いたとおり、iRobotアプリから確認することができます。

http://www.irobot.com との通信内容

Charles で通信内容覗いてみました。レスポンスのJSONの一例はこちら。

{
"termsUrl": "http:\/\/www.irobot.com\/roombaterms",
"privacyUrl": "",
"tosUrl": "",
"eulaUrl": "",
"version": "1.0"
}

termsUrlの値の URL をブラウザで開いてみると、Legal Informationが表示されますね。 これは iRobot アプリの このアプリについて > 利用規約および個人情報保護方針 を選択した時に表示されるページです。

image

どうやらこのドメインとの通信では、アプリ内のWebView等で表示するコンテンツの情報を取得しているようですね。

https://irobot.axeda.com との通信内容

というわけで、こちらのドメインがルンバのAPIである可能性が濃厚になってきました。 通信を覗いてみましょう。

同ドメインについては以下のURLに対してのみ通信を行っています。 https://irobot.axeda.com/services/v1/rest/Scripto/execute/AspenApiRequest

一番上の通信のリクエストパラメータを見てみます。

blid=xxxxxxxxxx&robotpwd=xxxxxxxxxxx&method=getStatus
※ blid と robotpwd のパスワードは値をマスクしています。

本記事では以降 blid を {{ ROOMBA_ID }}, robotpwd を {{ ROOMBA_PW }} と表記します。

どうやら、APIのURLは共通で、“method”パラメータの値で挙動を定義しているようですね。 レスポンスはこちらです。

{
"status": "OK",
"method": "getStatus",
"robotName": "kamoc",
"sku": "R980060",
"country": "JP",
"postalCode": "********(我が家の郵便番号)",
"regDate": "2015-11-05",
"manualUpdate": "",
"swUpdateAvailable": "",
"milestones": "",
"robotLanguage": "",
"tzName": "Asia/Tokyo",
"twoPass": "",
"openOnly": "",
"schedHold": "",
"carpetBoost": "",
"binPause": "",
"vacHigh": "",
"noPP": "",
"ecoCharge": "",
"mission": "{\"nMssn\":25,\"done\":\"ok\",\"flags\":0,\"sqft\":168,\"runM\":23,\"chrgM\":0,\"pauseM\":0,\"doneM\":0,\"dirt\":0,\"chrgs\":0,\"saves\":0,\"evacs\":0,\"pauseId\":0,\"wlBars\":[100,0,0,0,0]}",
"preventativeMaintenance": "[{\"partId\":\"bin\",\"date\":\"2015-11-05\",\"distance\":0,\"runtime\":0,\"months\":0,\"notified\":false},{\"partId\":\"core\",\"date\":\"2015-11-05\",\"distance\":0,\"runtime\":0,\"months\":0,\"notified\":false},{\"partId\":\"extractor\",\"date\":\"2015-11-05\",\"distance\":0,\"runtime\":0,\"months\":0,\"notified\":false}]",
"cleanSchedule": "{\"cycle\":[\"none\",\"none\",\"none\",\"none\",\"none\",\"none\",\"none\"],\"h\":[0,0,0,0,0,0,0],\"m\":[0,0,0,0,0,0,0]}",
"autoEvacCount": "",
"autoEvacFlags": "",
"wifiDiagnostics": "",
"autoEvacModel": "",
"robot_status": "{\"flags\":0,\"cycle\":\"none\",\"phase\":\"charge\",\"pos\":{\"theta\":174,\"point\":{\"x\":456,\"y\":9}},\"batPct\":100,\"expireM\":0,\"rechrgM\":0,\"error\":0,\"notReady\":0,\"mssnM\":23,\"sqft\":168}",
"softwareVersion": "v1.2.9",
"lastSwUpdate": "2015-11-09 18:32:02+0000",
"bbrun": "{\"hr\":7,\"min\":25,\"sqft\":24,\"nStuck\":2,\"nScrubs\":23,\"nPicks\":105,\"nPanics\":8,\"nCliffsF\":80,\"nCliffsR\":96,\"nMBStll\":0,\"nWStll\":1,\"nCBump\":0}",
"missing": false,
"ota": "{\"st\":0,\"err\":0,\"lbl\":\"\"}"
}

※ postalCode だけ値をマスクしています。

おぉ!いろいろと取れましたねぇ。 このAPIを定期的に呼び出せば、ルンバの現在の状態を知ることができそうですね。

次に、掃除開始命令を探してみましょう。 Charles さんに接続した状態で、iRobot アプリから掃除開始命令を出します。

image

はい。いましたねー。

iRobot APIの紹介

解析して明らかになった iRobot API をいくつか紹介します。

API の URL はすべて以下のURL。

https://irobot.axeda.com/services/v1/rest/Scripto/execute/AspenApiRequest

掃除開始

リクエストパラメータを以下のように設定

blid: "{{ ROOMBA_ID }}"
robotpwd: "{{ ROOOMBA_PW }}"
method: "multipleFieldSet"
value: { "remoteCommand" : "start" }

ステータス確認

リクエストパラメータを以下のように設定

blid: "{{ ROOMBA_ID }}"
robotpwd: "{{ ROOOMBA_PW }}"
method: "getStatus"

行動履歴確認

リクエストパラメータを以下のように設定

blid: "{{ ROOMBA_ID }}"
robotpwd: "{{ ROOOMBA_PW }}"
method: "missionHistory"

APIの調査が終わったところで、curlコマンドを使ってAPIを叩き、実際にルンバが動くかどうか試してみましょう。

curl コマンドを使った検証

試しに掃除開始APIを curl コマンドで叩いてみましょう。

curl https://irobot.axeda.com/services/v1/rest/Scripto/execute/AspenApiRequest -X POST -d "blid={{ROOMBA_ID}}&robotpwd={{ROOMBA_PW}}&method=multipleFieldSet&value=%7b%22remoteCommand%22%20%3a%20%22start%22%7d"
{"status":"OK","method":"multipleFieldSet"}%

valueのパラメータは JSON 文字列をURLエンコードしています。

コンソールのEnterキーをターン!と叩きます。

・・・(2,3秒間があく)

ルンバ「ててーててー♪ ぴー ぴー ぴー ぴー」==◎

ルンバが走り出しました!!

ここまできたら後は Slack BOT とつなぐだけですね。

Slack bot の作成

Slack bot の作成は以下のサイトを参考にしました。

Coffeeスクリプトはこんな感じです。

module.exports = (robot) ->
robot.respond /((掃除))/i, (msg) ->
data = 'blid={{ROOMBA_ID}}&robotpwd={{ROOMBA_PW}}&method=multipleFieldSet&value=%7b%22remoteCommand%22%20%3a%20%22start%22%7d'
msg.http('https://irobot.axeda.com/services/v1/rest/Scripto/execute/AspenApiRequest')
.header('Content-Type', 'application/x-www-form-urlencoded')
.post(data) (err, res, body) ->
json = JSON.parse body
if json.status == "OK"
msg.send "仕方ないね"
else
msg.send "ルンバに問題が発生しているよ"

Slack bot のソースコード一式はGitHubにあげてあります。

{{ROOMBA_ID}} と {{ROOMBA_PW}} だけご自身のルンバの値に書き換えてください。

社内 Slack で運用してみた

弊社 Slack にて世界中どこからでも、何時でも、kamoc宅のルンバを起動できるという状態にしてみました。

弊社社員に可愛がってもらっている様子がこちらです。

image

夜11:39に福島県からのリクエストで静岡県のルンバが掃除しております。

これはもう事件ですね。

セキュリティーの重要性について改めて考えさせられました。

現在1週間程運用しておりますが、深夜の掃除で目が覚めるといった自体は起きておりません。

SlackBOTをホストしているHerokuさんがスリープ状態になってしまい、呼びかけに反応しないという問題が発生するため、設定の調整が必要です。

それでは、今日はこの辺で。

ルンバかわいいよ、ルンバ。