ifttt
RaspberryPi
bottle
IoT
Mesh
IAMASDay 11

Raspberry Pi向けMESHハブアプリとMESH SDKでlocalhostと通信する

はじめに

MESHは、ソニー株式会社のMESHプロジェクトがつくった、リアルなブロック「MESHタグ」とスマートフォンやタブレット用のアプリ「MESHアプリ」からなるIoTツールキットです。発売後の継続的なアップデートでIFTTTにも対応し、初めてセンサやアクチュエータに触れる人々でも抵抗なく短時間で理解できるため、IoTの概念を体験しながら理解するための活動における主要なツールとして2016年から私自身では積極的に活用してきました。

今年の8月には、こうした活動の中で生まれたツールをMESHプロジェクトと協働でブラッシュアップしたものを「MESHデザインパターンカード」としてリリースしました(IAMASからは私と産業文化研究センターの高見知里さんが参加しました)。MESHデザインパターンカードは、参加者(たち)が見つけた課題やつくりたいものと、MESHでできることを掛け合わせてアイデアを発展させる場面で使用するカードです。それぞれのカードの表面にはMESHを使ってできることが、裏面にはその実現方法が説明されています。入力→出力、入力→処理→出力のように複数のカードを並べることにより、一見すると複雑な流れを整理できます。

IMG_5158.jpg

IMG_5159.jpg

MESHとMESHデザインパターンカードを組み合わせることにより、1時間程度という限られた時間の中でも初めて触れた参加者たちがIFTTTと組み合わせてIoTアプリケーションのアイデアを実際に体験できるところまでできることを何度も確認しています(例:新たな技術を福祉にいかす体験ワークショップ)。

このように、MESHを通じてIoTというテクノロジーを体験してみるところまでは非常にスムーズに導入できるようになりました。しかしながら、実際に現場に導入して経験にしていくためには様々な課題がありました。大きなものの一つが、MESHを使うのにスマートフォンかタブレットが必須だったことです。短時間のワークショップであれば自分たちが普段使っているものを流用できますが、長期間の運用となると(たまたま機種変更後のスマートフォンが余っているとか出ない限り)専用に割り当てることは難しいでしょう。

そんな中、2017年12月8日にMESHプロジェクトから「Raspberry Pi向けMESHハブアプリケーション(MESHハブ)」がリリースされました。これにより、MESHアプリが動作するスマートフォンかタブレットでレシピを作成したあとは、MESHハブアプリが動作するRaspberry Piで常時運用できるようになりました。早速、「Raspberry Pi向けMESHハブアプリケーションマニュアル」に書かれているとおりに設定して試してみると確かに動作します。これで今まであった課題の一つが解決しましたので、別の課題(と感じるかどうかは人それぞれだと思いますが)に取り組んでみることにしました。

課題

2015年に発売された当初よりMESHにはオンラインのSDK「MESH SDK」が用意され、以前の記事「MESHのカスタムタグで時間を計測してGoogle スプレッドシートにデータをアップロードする」で紹介したような機能拡張を行うことができました。しかしながら、このSDKを使うにはいくつかの制約があります。まず、プログラミング言語はJavaScriptに限定されます。また、拡張タグの開発過程で試行錯誤するには、MESH SDKのウェブサイトでファイルを編集し、MESHアプリ上で該当する拡張タグを選択して更新を実行し、動作結果をLogで確認する、というステップを繰り返す必要があります。

実装

今回は、MESHハブアプリをローカルサーバと通信させることにより、ローカルで素速く試行錯誤できるようにしつつ、さらに複雑な拡張が必要になったときにも対応できるようにするための土台をつくってみることにしました。その一例として、以前の記事で紹介したストップウォッチを別の形で実装してみました。MESHハブアプリと拡張タグで実装する部分は最小限にして、大半の処理をローカルサーバで行うようにしたことにより、一度作成した拡張タグは一切変更せず、ローカルサーバのコードを変更するだけで素速く試行錯誤や機能拡張できるようにしました。

sequence_diagram.png

拡張タグ

オンラインのMESH SDKで作成した拡張タグの設定です。コネクタは入力が1つ、出力は無しに設定しています。

image.png

プロパティはローカルサーバのパスだけです(プロパティでポート番号を設定できるようにしてもいいかもしれません)。

image.png

コードはExecuteメソッドのみ記述しています。

image.png

Executeメソッドのコードは以下のようになっています。http://localhost:8080に対してプロパティで設定したパスを追加したURLにアクセスするだけですので、非常に簡単です。

Execute
var localhost = 'http://localhost:8080' + properties.path;

ajax({
  url: localhost,
  type: 'get',
  timeout: 5000,
  success: function(contents) {
    log(contents);

    callbackSuccess({
      resultType: 'continue',
    });
  },
  error: function(request, errorMessage) {
    log('ERROR: ' + errorMessage);

    callbackSuccess({
      resultType: 'continue',
    });
  }
});

return {
  resultType: 'pause'
};

参考用として、MESH SDKのウェブサイトでインポートできるようにエクスポートしたJSONデータも掲載しておきます。

local
{"formatVersion":"1.0","tagData":{"name":"local","icon":"./res/x2/default_icon.png","description":"Make a webhook to a local server that is running on port number 8080","functions":[{"id":"function_0","name":"Webhook","connector":{"inputs":[{"label":"Request"}],"outputs":[]},"properties":[{"name":"Path","referenceName":"path","type":"string","defaultValue":"/path"}],"extension":{"initialize":"","receive":"","execute":"var localhost = 'http://localhost:8080' + properties.path;\n\najax({\n  url: localhost,\n  type: 'get',\n  timeout: 5000,\n  success: function(contents) {\n    log(contents);\n\n    callbackSuccess({\n      resultType: 'continue',\n    });\n  },\n  error: function(request, errorMessage) {\n    log('ERROR: ' + errorMessage);\n\n    callbackSuccess({\n      resultType: 'continue',\n    });\n  }\n});\n\nreturn {\n  resultType: 'pause'\n};","result":""}}]}}

以上で拡張タグが準備できましたので、MESHアプリに拡張タグを追加し、ボタンタグの1回押し、2回押し、長押しにそれぞれ1つづつを割り当て、/stopwatch/start/stopwatch/lap/stopwatch/stopというパスを設定します。これにより、ボタンタグのボタンを操作するとローカルホストのポート番号8080でそれぞれに対応したパスにアクセスするようになります。

mesh_recipe.PNG

ローカルサーバ

ローカルサーバをどの言語で実装するかは自由ですので、今回は機械学習などでもよく用いられるPythonで記述してみます。Pythonでウェブサーバを構築するための軽量なフレームワークとして知られているものにFlaskBottleがあります。ポピュラーなのはFlaskのようですが、今回はものすごく単純なことしか実装しないため、標準以外のライブラリに依存しないBottleを選択しました。インストールはpipで簡単にできます。

$ pip3 install bottle

Bottleを使用することにより、以下のように記述するだけでローカルサーバをポート番号8080で起動してアクセスされたパスに応じた処理を行えます。

from bottle import route, run, template

@route('/stopwatch/<command>')
def stopwatch(command):
    if command == 'start':
        # /stopwatch/startに対応する処理
    elif command == 'lap':
        # /stopwatch/lapに対応する処理
    elif command == 'stop':
        # /stopwatch/stopに対応する処理

run(host='localhost', port=8080)

全体は以下のようになっています。

server.py
from bottle import route, run, template
import requests
import time


class Stopwatch:
    def __init__(self):
        self.start_time = 0
        self.lap_time = 0
        self.start_to_lap = 0
        self.lap_to_stop = 0

    def start(self):
        self.start_time = time.time()
        self.lap_time = self.start_time

    def lap(self):
        self.lap_time = time.time()

    def stop(self):
        stop_time = time.time()
        self.start_to_lap = self.lap_time - self.start_time
        self.lap_to_stop = stop_time - self.lap_time


IFTTT_KEY = '*********************'
IFTTT_EVENT = 'stopwatch'
IFTTT_URL = 'https://maker.ifttt.com/trigger/{event}/with/key/{key}'.format(
    event=IFTTT_EVENT, key=IFTTT_KEY)


def make_web_request(start_to_lap, lap_to_stop):
    data = {}
    data['value1'] = start_to_lap
    data['value2'] = lap_to_stop

    try:
        response = requests.post(IFTTT_URL, data=data)
        print('{0.status_code}: {0.text}'.format(response))
    except:
        print('Failed to make a web request')


sw = Stopwatch()


@route('/stopwatch/<command>')
def stopwatch(command):
    if command == 'start':
        sw.start()
        return template('{{command}} requested', command=command)
    elif command == 'lap':
        sw.lap()
        return template('{{command}} requested', command=command)
    elif command == 'stop':
        sw.stop()
        make_web_request(round(sw.start_to_lap, 1), round(sw.lap_to_stop, 1))
        message = 'start to lap was {0} sec., lap to stop was {1} sec.'.format(
            round(sw.start_to_lap, 1),
            round(sw.lap_to_stop, 1))
        return template('{{message}}', message=message)

    return template('{{command}} is an unknown command', command=command)


run(host='localhost', port=8080)

おわりに

今後MESHと機械学習を組み合わせようと考えていたこともあり、今回はローカルサーバをPythonで実装しました。しかしながら、JavaScriptを始めとする他のテキストベースのプログラミング言語でも、Node-REDのようなグラフィカルなプログラミング環境でも同様に実現できるでしょう。また、今回は触れませんでしたが、今回と同じ方法でRaspberry Piに接続したカメラやサウンドをコントロールすることも可能です。

くわえて、さらに他のローカルサーバを用意して拡張することも可能です。例えば、Google Homeに任意のメッセージを通知できるgoogle-home-notifierを利用して、IFTTTで記録すると共に音声で結果を読み上げるようにも簡単に変更できます。Raspberry PiとGoogle Homeは同じネットワーク内にあれば物理的に離れていても大丈夫ですので、離れた場所に音声で通知することも可能になります。

GOOGLE_HOME_IP = '192.168.1.20' # Google Homeアプリで取得したGoogle Homeのアドレス
LANGUAGE = 'us'
LISTENER_URL = 'http://localhost:8091/google-home-notifier?ip={ip}&language={language}'.format(
    ip=GOOGLE_HOME_IP, language=LANGUAGE)


def notify_google_home(message):
    try:
        data = {'text': message}
        response = requests.post(LISTENER_URL, data=data)
        print('{0.status_code}: {0.text}'.format(response))
    except:
        print('Failed to make a web request')


@route('/stopwatch/<command>')
def stopwatch(command):
    ...
    elif command == 'stop':
        sw.stop()
        make_web_request(round(sw.start_to_lap, 1), round(sw.lap_to_stop, 1))
        message = 'start to lap was {0} sec., lap to stop was {1} sec.'.format(
            round(sw.start_to_lap, 1),
            round(sw.lap_to_stop, 1))
        notify_google_home(message)
        return template('{{message}}', message=message)

    return template('{{command}} is an unknown command', command=command)

次の機会があれば、もう少し発展させた例を紹介してみたいと思います。

リファレンス