「@Roomba 掃除しろ」 Slack BOT から新型ルンバ980をWiFiで制御する

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

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

最初に

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

image

今回のゴール

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

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

通信内容を観察してみる

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

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

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(www.irobot.com/roombaterms)をブラウザで開いてみると、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さんがスリープ状態になってしまい、呼びかけに反応しないという問題が発生するため、設定の調整が必要です。

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

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