AIY Voice Kit + Web カメラで音声で写真を撮る

 前回は Voice Kit にブレッドボードとLEDをつないで音声で操作してみました。今回は手持ちの Web カメラを繋げて音声で操作して写真を撮ってみたいと思います。

f:id:akanuma-hiroaki:20180201091154j:plain:w450

Web カメラの接続

 今回はUSB接続の Web カメラを使用します。まず接続前の USB デバイスの認識状況です。

pi@raspberrypi:~ $ lsusb
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ ls /dev/video*
ls: '/dev/video*' にアクセスできません: そのようなファイルやディレクトリはありません

 そして Raspberry Pi の USB ポートに Web カメラを接続した後は下記のようになります。

pi@raspberrypi:~ $ lsusb
Bus 001 Device 004: ID 056e:700f Elecom Co., Ltd 
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ ls /dev/video*
/dev/video0

 Device 004 として Elecom の Web カメラが認識され、デバイスファイルも作成されました。とりあえずのカメラの動作テスト用に、 guvcview をインストールします。

pi@raspberrypi:~ $ sudo apt-get install guvcview

 無事にインストールできたら下記のように起動します。 ssh ではなく VNC か直接デスクトップにログインして実行する必要があります。

pi@raspberrypi:~ $ guvcview &

 起動すると Web カメラの映像がデスクトップ上に表示されます。

f:id:akanuma-hiroaki:20180202075850p:plain:w450

OpenCV のインストールと動作確認

 今回は Web カメラからの画像のキャプチャ用に OpenCV を使います。

opencv.org

 Voice Kit ではスクリプトを実行する場合にデスクトップの Start dev terminal ショートカットからシェルを起動します。

f:id:akanuma-hiroaki:20180201082622p:plain:w450

 このショートカットの中身は下記のようになっていて、 /home/pi/bin/AIY-projects-shell.sh が実行されていますので、 VNC ではなく ssh 等でログインしている場合にはこの AIY-projects-shell.sh を実行してシェルを起動してからスクリプトを実行します。

[Desktop Entry]
Encoding=UTF-8
Type=Application
Name=Start dev terminal
Commment=Start a terminal with the environment for the AIY projects kit.
Exec=/home/pi/bin/AIY-projects-shell.sh
Terminal=true
Icon=utilities-terminal

 AIY-projects-shell.sh の中身は下記のようになっていて、 env/bin/activate で Voice Kit のコード実行用の環境設定が行われています。

#!/bin/bash --rcfile

source /etc/bash.bashrc
source ~/.bashrc

cat /etc/aiyprojects.info

cd ~/AIY-projects-python
source env/bin/activate

echo "Dev terminal is ready! See the demos in 'src/examples' to get started."

 AIY-projects-shell.sh を起動して環境設定が行われると $PATH の先頭は /home/pi/AIY-projects-python/env/bin になり、 python や pip 等のコマンドもそちらのものが参照されるようになります。

(env) pi@raspberrypi:~ $ which python
/home/pi/AIY-projects-python/env/bin/python
(env) pi@raspberrypi:~ $ which pip3
/home/pi/AIY-projects-python/env/bin/pip3

 なので OpenCV のインストールも AIY-projects-shell.sh を起動した後に pip でインストールします。

(env) pi@raspberrypi:~ $ pip3 install opencv-python
Collecting opencv-python
  Using cached https://www.piwheels.hostedpi.com/simple/opencv-python/opencv_python-3.3.0.10-cp35-cp35m-linux_armv7l.whl
Requirement already satisfied: numpy>=1.12.1 in /usr/lib/python3/dist-packages (from opencv-python)
Installing collected packages: opencv-python
Successfully installed opencv-python-3.3.0.10

 インストールされたら簡単に動作確認をしてみます。

(env) pi@raspberrypi:~ $ python -c 'import cv2'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/pi/AIY-projects-python/env/lib/python3.5/site-packages/cv2/__init__.py", line 9, in <module>
    from .cv2 import *
ImportError: libcblas.so.3: cannot open shared object file: No such file or directory

 libcblas.so.3 がないと言われてしまったので、 libatlas-base-dev を apt-get でインストールします。

(env) pi@raspberrypi:~ $ sudo apt-get install libatlas-base-dev 

 再度動作確認してみます。

(env) pi@raspberrypi:~ $ python -c 'import cv2'                                                                                                                                                                   
(env) pi@raspberrypi:~ $ 

 ライブラリが参照できるようになったようなので、インタラクティブシェルで動作を確認してみます。

(env) pi@raspberrypi:~ $ python
Python 3.5.3 (default, Jan 19 2017, 14:11:04) 
[GCC 6.3.0 20170124] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
>>> import cv2
>>> 
>>> c = cv2.VideoCapture(0)
>>> 
>>> r, img = c.read()
>>> 
>>> cv2.imwrite('capture.jpg', img)
True
>>> 

 キャプチャ画像が capture.jpg というファイルに保存されていれば正常に動作しています。今回は下記のような画像がキャプチャされました。

f:id:akanuma-hiroaki:20180202082309j:plain:w450

サンプルコード実行時のエラー

 途中で Voice Kit で提供されているサンプルコードを動かしてみたところ、下記のようにエラーになってしまいました。

(env) pi@raspberrypi:~/AIY-projects-python $ src/examples/voice/assistant_library_demo.py 
ALSA lib pcm.c:8403:(snd_pcm_set_params) Rate doesn't match (requested 16000Hz, get 48000Hz)
[1013:1028:ERROR:audio_input_processor.cc(755)] Input error
/home/pi/AIY-projects-python/src/aiy/_drivers/_led.py:51: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(channel, GPIO.OUT)
Say "OK, Google" then speak, or press Ctrl+C to quit...
[1013:1031:ERROR:audio_input_processor.cc(755)] Input error
Segmentation fault

 オーディオ入力のサンプリングレートがマッチしないという感じのエラーのようです。ググってみたところ下記の情報を見つけました。

github.com

 .asoundrc の設定内容が合っていないということのようなので、 /etc/asound.conf をコピーして使用します。

pi@raspberrypi:~ $ cat .asoundrc 
pcm.!default {
        type hw
        card 0
}

ctl.!default {
        type hw
        card 0
}
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat /etc/asound.conf 
options snd_rpi_googlevoicehat_soundcard index=0

pcm.softvol {
    type softvol
    slave.pcm dmix
    control {
        name Master
        card 0
    }
}

pcm.micboost {
    type route
    slave.pcm dsnoop
    ttable {
        0.0 30.0
        1.1 30.0
    }
}

pcm.!default {
    type asym
    playback.pcm "plug:softvol"
    capture.pcm "plug:micboost"
}

ctl.!default {
    type hw
    card 0
}
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ mv .asoundrc .asoundrc.20180202
pi@raspberrypi:~ $ cp /etc/asound.conf .asoundrc

 これで無事動作するようになりました。

スクリプト実装

 動作確認が取れたのでスクリプトを実装していきます。前回の LED のスクリプトの発話内容による分岐に下記の処理を追加します。「Take a picture.」と発話するとカウントダウンして写真を撮影します。また、カウントダウンに合わせて LED も使用してみました。

elif text == 'take a picture':
    assistant.stop_conversation()
    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
    GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)

    now = datetime.now()
    filename = 'capture_%s.jpg' % now.strftime('%Y%m%d_%H%M%S')

    aiy.audio.say('OK, 3')
    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
    time.sleep(1)

    aiy.audio.say('2')
    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
    GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
    time.sleep(1)

    aiy.audio.say('1')
    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
    time.sleep(1)

    c = cv2.VideoCapture(0)
    r, img = c.read()
    if cv2.imwrite(filename, img):
        self.print_and_say('I took nice one.')
    else:
        self.print_and_say("I couldn't take a picture.")

    c.release()
    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
    GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)
    print('Captured Image: %s' % filename)

 スクリプト全体は下記のようになります。

#!/usr/bin/env python3

import sys
import time
import cv2
from datetime import datetime

import aiy.assistant.auth_helpers
import aiy.audio
import aiy.voicehat
from google.assistant.library import Assistant
from google.assistant.library.event import EventType

import RPi.GPIO as GPIO

class MyAssistant:
    GPIO_LED_GREEN = 2
    GPIO_LED_YELLOW = 3

    def __init__(self):
        self.print_and_say('Initializing MyAssistant.')
        self.credentials = aiy.assistant.auth_helpers.get_assistant_credentials()
        self.status_ui = aiy.voicehat.get_status_ui()

        GPIO.setmode(GPIO.BCM)
        GPIO.setup(MyAssistant.GPIO_LED_GREEN, GPIO.OUT)
        GPIO.setup(MyAssistant.GPIO_LED_YELLOW, GPIO.OUT)

    def print_and_say(self, text):
        print(text)
        aiy.audio.say(text)

    def process_event(self, assistant, event):
        print('Processing event. The event is %s.' % event.type)

        if event.type == EventType.ON_START_FINISHED:
            self.status_ui.status('ready')
            if sys.stdout.isatty():
                print('Say "OK, Google" then speak.')
        elif event.type == EventType.ON_CONVERSATION_TURN_STARTED:
            self.status_ui.status('listening')
        elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED and event.args:
            print('You said: %s' % event.args['text'])
            text = event.args['text'].lower()

            if text == 'turn on green led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                self.print_and_say('Turned on green LED.')
            elif text == 'turn off green led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                self.print_and_say('Turned off green LED.')
            elif text == 'turn on yellow led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
                self.print_and_say('Turned on yellow LED.')
            elif text == 'turn off yellow led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)
                self.print_and_say('Turned off yellow LED.')

            elif text == 'turn on all led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
                self.print_and_say('Turned on all LED.')
            elif text == 'turn off all led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)
                self.print_and_say('Turned off all LED.')
            elif text == 'take a picture':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)

                now = datetime.now()
                filename = 'capture_%s.jpg' % now.strftime('%Y%m%d_%H%M%S')

                aiy.audio.say('OK, 3')
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                time.sleep(1)

                aiy.audio.say('2')
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
                time.sleep(1)

                aiy.audio.say('1')
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                time.sleep(1)

                c = cv2.VideoCapture(0)
                r, img = c.read()
                if cv2.imwrite(filename, img):
                    self.print_and_say('I took nice one.')
                else:
                    self.print_and_say("I couldn't take a picture.")

                c.release()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)
                print('Captured Image: %s' % filename)
            elif text == 'goodbye':
                self.status_ui.status('stopping')
                assistant.stop_conversation()
                aiy.audio.say('Goodbye. See you again.')
                print('Stopping...')
                sys.exit()

        elif event.type == EventType.ON_END_OF_UTTERANCE:
            self.status_ui.status('thinking')
        elif event.type == EventType.ON_CONVERSATION_TURN_FINISHED:
            self.status_ui.status('ready')
        elif event.type == EventType.ON_ASSISTANT_ERROR and event.args and event.args['is_fatal']:
            sys.exit(1)

    def main(self):
        self.status_ui.status('starting')
        self.print_and_say('Starting main method.')
        with Assistant(self.credentials) as assistant:
            for event in assistant.start():
                self.process_event(assistant, event)

if __name__ == '__main__':
    sample = MyAssistant()
    sample.main()

動作確認

 スクリプトを実行して動作を確認してみます。下記の動画のような動作になりました。

 撮影された写真は下記のようになります。

f:id:akanuma-hiroaki:20180202090308j:plain:w450

まとめ

 USBカメラの接続も特に苦労することなく、 OpenCV でのキャプチャーもすんなり行えました。この辺りの自由度が Google Home と違う面白さかと思います。今回の内容だと撮影してみないとどんな画像かがわからないので、映像を確認しながら撮影できるようにしたり、撮影後にデスクトップに画像を表示するなどの対応を試してみたいと思います。

AIY Voice Kit にブレッドボードをつないで音声で LED を操作する

 前回 Google AIY Voice Kit を購入してマニュアル通りに組み立ててサンプルプログラムを動かすところまでやってみましたが、やはり Voice Kit が Google Echo と違って面白いのは Raspberry Pi ベースであるからこその自由度ということで、今回はとりあえずブレッドボードで LED を接続して、音声で LED を操作してみました。

GPIO ピンヘッダの取り付け

 AIY Voice Kit では Raspberry Pi に Voice HAT Accessory Board をマウントしているので、 Raspberry Pi の GPIO は全て覆われてしまっています。なので GPIO で接続する場合には Voice HAT の方の GPIO を使用することになります。 Voice HAT の GPIO Pinout はドキュメントでも図で説明されています。

aiyprojects.withgoogle.com

 図の下の方には表形式で各ピンについての説明が書かれています。今回は LED を二つ接続してみますので、GPIO02, 03 を使ってみます。 I2C としても使えるピンなので、ボード上のプリントは I2C になっています。写真赤枠のピンの右から GPIO02, GPIO03, GND になります。

f:id:akanuma-hiroaki:20180123223632j:plain:w450

 Voice HAT にはピンがついていないので、別途ピンヘッダを用意して、一度 Voice HAT を Raspberry Pi から外して半田付けします。

f:id:akanuma-hiroaki:20180123224112j:plain:w450

 半田付けしたらジャンパーコードを接続しておきます。

f:id:akanuma-hiroaki:20180123224336j:plain:w450

 そして再度 Voice Kit を組み上げ、ジャンパーコードの先を外へ引っ張り出しておきます。

f:id:akanuma-hiroaki:20180123224503j:plain:w300

ブレッドボードと LED の取り付け

 先ほど引っ張り出しておいたジャンパーコードから、下記の写真のようにブレッドボードとLEDを接続します。緑色のコードが GPIO02、黄色のコードが GPIO03 でそれぞれ LED のアノード側へ接続し、黒いコードが GND の接続になります。

f:id:akanuma-hiroaki:20180123225024j:plain:w300

スクリプト実装

 それでは Python のスクリプトを実装します。公式から提供されている下記サンプルをベースに、GPIOの操作などを追加しています。

github.com

 まずスクリプト全体は下記の通りです。

#!/usr/bin/env python3

import sys

import aiy.assistant.auth_helpers
import aiy.audio
import aiy.voicehat
from google.assistant.library import Assistant
from google.assistant.library.event import EventType

import RPi.GPIO as GPIO

class MyAssistant:
    GPIO_LED_GREEN = 2
    GPIO_LED_YELLOW = 3

    def __init__(self):
        self.print_and_say('Initializing MyAssistant.')
        self.credentials = aiy.assistant.auth_helpers.get_assistant_credentials()
        self.status_ui = aiy.voicehat.get_status_ui()

        GPIO.setmode(GPIO.BCM)
        GPIO.setup(MyAssistant.GPIO_LED_GREEN, GPIO.OUT)
        GPIO.setup(MyAssistant.GPIO_LED_YELLOW, GPIO.OUT)

    def print_and_say(self, text):
        print(text)
        aiy.audio.say(text)

    def process_event(self, assistant, event):
        print('Processing event. The event is %s.' % event.type)

        if event.type == EventType.ON_START_FINISHED:
            self.status_ui.status('ready')
            if sys.stdout.isatty():
                print('Say "OK, Google" then speak.')
        elif event.type == EventType.ON_CONVERSATION_TURN_STARTED:
            self.status_ui.status('listening')
        elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED and event.args:
            print('You said: %s' % event.args['text'])
            text = event.args['text'].lower()

            if text == 'turn on green led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                self.print_and_say('Turned on green LED.')
            elif text == 'turn off green led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                self.print_and_say('Turned off green LED.')
            elif text == 'turn on yellow led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
                self.print_and_say('Turned on yellow LED.')
            elif text == 'turn off yellow led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)
                self.print_and_say('Turned off yellow LED.')
            elif text == 'turn on all led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
                self.print_and_say('Turned on all LED.')
            elif text == 'turn off all led':
                assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)
                self.print_and_say('Turned off all LED.')
            elif text == 'goodbye':
                self.status_ui.status('stopping')
                assistant.stop_conversation()
                aiy.audio.say('Goodbye. See you again.')
                print('Stopping...')
                sys.exit()

        elif event.type == EventType.ON_END_OF_UTTERANCE:
            self.status_ui.status('thinking')
        elif event.type == EventType.ON_CONVERSATION_TURN_FINISHED:
            self.status_ui.status('ready')
        elif event.type == EventType.ON_ASSISTANT_ERROR and event.args and event.args['is_fatal']:
            sys.exit(1)

    def main(self):
        self.status_ui.status('starting')
        self.print_and_say('Starting main method.')
        with Assistant(self.credentials) as assistant:
            for event in assistant.start():
                self.process_event(assistant, event)

if __name__ == '__main__':
    sample = MyAssistant()
    sample.main()

 Assistant の start() メソッドを実行することで Google Assistant が hotword を待ち受け、検知するとイベントが発生しますので、イベントを process_event() メソッドに渡して処理します。

with Assistant(self.credentials) as assistant:
    for event in assistant.start():
        self.process_event(assistant, event)

 イベントの種別についてはこちらに記載があります。

Google Assistant Library  |  Google Assistant SDK  |  Google Developers

 ユーザが hotword の後に発話した内容は ON_RECOGNIZING_SPEECH_FINISHED イベントのパラメータとして渡されますので、その内容によって処理を切り分け、 GPIO の操作をして LED をコントロールしています。ポイントは assistant.stop_conversation() を実行しているところで、これを行うことによって Google Assistant による処理を中断し、ローカルで行わせたい処理だけ行なっています。 stop_conversation() を実行しないと発話内容に対して Google Assistant でも処理を行おうとするので、そのコマンドには対応できない的な返答が返されてしまいます。

if text == 'turn on green led':
    assistant.stop_conversation()
    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
    self.print_and_say('Turned on green LED.')

動作確認

 上記のスクリプトを実行して hotword に続いて発話すると、下記の動画のように LED を操作することができます。

まとめ

 Voice HAT へのピンヘッダの半田付けは少々手間ですが、それ以外の点ではすんなり GPIO を使うことができました。今回は LED の出力のコントロールだけでしたが、情報を入力するセンサーデバイスも使ってもう少し遊んでみたいと思います。

Google AIY Voice Kit を試してみる

 Google AIY Projects の Voice Kit を買ってみたのでとりあえず試してみました。

aiyprojects.withgoogle.com

 こんな感じのパッケージで届きます。

f:id:akanuma-hiroaki:20180113200108j:plain:w450

 別途 Raspberry Pi や micro SD カード等も必要なので、単純にスマートスピーカーとして使うだけなら Google Home mini を買った方が安いしお手軽なのですが、 Raspberry Pi 上でスクリプトを作成したり、 GPIO 等でセンサーデバイス等と連携させることもできるのが面白そうだったので購入してみました。今回はひとまず組み立てと動作確認を行ってみます。

 基本的な手順は下記 Google の製品ページで紹介されています。

https://aiyprojects.withgoogle.com/voice/#assembly-guide

 KSY で購入ですると日本語版の組み立てガイドが付属しているのでそれに従って進めます。

https://raspberry-pi.ksyic.com/main/index/pdp.id/331/pdp.open/331

f:id:akanuma-hiroaki:20180113201618j:plain:w450

OS イメージの準備

 キットの組み立ての前に SD カードに OS のイメージを書き込んでおきます。今回は下記イメージをダウンロードして使用しました。

https://dl.google.com/dl/aiyprojects/vision/aiyprojects-2018-01-03.img.xz

 書き込みには Etcher を使用します。

etcher.io

 Etcher を起動して Select Image をクリックしてダウンロードしておいたOSイメージを選択します。

f:id:akanuma-hiroaki:20180113141124p:plain:w450

 SDカードをリーダーに挿入してあればドライブが自動的に選択されていますので、 Flash! をクリックして書き込みを実行します。ドライブが適切に選択されていない場合は Change をクリックして対象のドライブを選択します。

f:id:akanuma-hiroaki:20180113141142p:plain:w450

 下記のような表示になれば無事完了です。

f:id:akanuma-hiroaki:20180113141133p:plain:w450

組み立て

 それではキットを組み立てていきます。箱を開けるとダンボールの筐体や各種パーツ、組み立てガイド(英語)が入っています。

f:id:akanuma-hiroaki:20180113202302j:plain:w450

 まずは GPIO 等の端子を備えたメインのボードである Voice HAT Accessory Board です。

f:id:akanuma-hiroaki:20180113201847j:plain:w450

 これをスペーサーを使って Raspberry Pi にマウントします。

f:id:akanuma-hiroaki:20180113201920j:plain:w450

 そしてスピーカーを Voice HAT の端子に精密ドライバーを使って接続します。

f:id:akanuma-hiroaki:20180113202041j:plain:w450

 次はマイク基盤です。このマイクでユーザの音声入力を受け付けます。

f:id:akanuma-hiroaki:20180113202119j:plain:w450

 これも Voice HAT に接続します。

f:id:akanuma-hiroaki:20180113202155j:plain:w450

 そして外側の筐体と内側のフレームを組み立てます。

f:id:akanuma-hiroaki:20180113202228j:plain:w450

 内側のフレームと先ほどまでに組み立てた基盤やスピーカーを組み合わせて筐体に入れます。

f:id:akanuma-hiroaki:20180113202416j:plain:w450

 上部のボタンを取り付けて箱を閉じれば完成です。

f:id:akanuma-hiroaki:20180113202521j:plain:w450

デバイスの動作確認

 OSイメージを書き込んでおいた micro SD カードを Raspberry Pi に挿入し、USBでキーボードとマウスを接続し、HDMI でモニタを接続したら電源ケーブルを接続して起動します。 Wi-Fi の接続や SSH, VNC の接続が必要であれば設定しておきます。

f:id:akanuma-hiroaki:20180113141153p:plain:w450

 公式のOSイメージを使用していれば動作確認用のスクリプトが用意されています。まずはオーディオデバイスの動作確認用スクリプトです。デスクトップの下記スクリプトをダブルクリックで実行します。

f:id:akanuma-hiroaki:20180115220156p:plain:w450

 実行すると下記のようにスクリプトが実行され、テスト用の音声が再生されますので、スピーカーから聞こえていれば y を入力します。続いてマイクのテストも行われますので、 Enter を押して「Testing, 1 2 3」と喋ります。正しくマイクが機能していれば今喋った内容が再生されますので、 y を入力してテスト終了です。

Enabling audio driver for VoiceKit.
Playing a test sound...
Did you hear the test sound? (y/n) y
When you're ready, press enter and say 'Testing, 1 2 3'...
Recording...
Playing back recorded audio...
Did you hear your own voice? (y/n) y
The audio seems to be working.
Press Enter to close...

 次に Wi-Fi の接続テストです。デスクトップの下記スクリプトをダブルクリックで実行します。

f:id:akanuma-hiroaki:20180115230018p:plain:w450

 実行すると下記のように Wi-Fi の接続確認が行われます。

Checking the WiFi connection...
Trying to contact Google's servers...
The WiFi connection seems to be working.
Press Enter to close...

サンプルプログラムの実行

 公式の OS イメージでは Google Assistant SDK のサンプルプログラムが提供されているのでそれを実行してみます。まずは Google Assistant API を有効にする必要がありますので、Google Cloud Platform(GCP) のコンソールにアクセスして、新しいプロジェクトを作成します。

f:id:akanuma-hiroaki:20180115230834p:plain:w450

 プロジェクト名を入力して 作成 ボタンをクリックします。

f:id:akanuma-hiroaki:20180115230911p:plain:w450

 プロジェクトが作成されたら「API とサービス」メニューから API とサービスの有効化 をクリックします。

f:id:akanuma-hiroaki:20180115230930p:plain:w450

 Google Assistant API を検索してクリックします。

f:id:akanuma-hiroaki:20180115230949p:plain:w450

 API の詳細画面で 有効にする をクリックします。

f:id:akanuma-hiroaki:20180115231008p:plain:w450

 次に認証情報を作成する必要があるので、「認証情報」メニューの画面で 認証情報を作成 プルダウンから OAuth クライアント ID を選択します。

f:id:akanuma-hiroaki:20180115231054p:plain:w450

 最初は同意画面を作成する必要があるので、 同意画面を設定 をクリックします。

f:id:akanuma-hiroaki:20180115231114p:plain:w450

 入力フォームでユーザーに表示するサービス名を設定します。それ以外はそのままで構いませんので、 保存 をクリックします。

f:id:akanuma-hiroaki:20180115231156p:plain:w450

 クライアントIDの作成画面に戻ったら名前を入力して 作成 ボタンをクリックします。

f:id:akanuma-hiroaki:20180115231214p:plain:w450

 これで認証情報の作成は完了ですので、一覧画面からダウンロードボタンをクリックして json ファイルをダウンロードし、 /home/pi/assistant.json として保存しておきます。

f:id:akanuma-hiroaki:20180115231232p:plain:w450

 サンプルプログラム実行の準備ができましたので、デスクトップから Start dev terminal を実行します。

f:id:akanuma-hiroaki:20180116074957p:plain:w450

 ターミナルが起動しますので、下記のようにサンプルプログラムを実行します。初回起動時は OAuth での認証が必要ですのでブラウザ上で認証します。認証が完了するとプログラムが実行されますので、 Wake Word「OK, Google」を使って話しかけます。ちなみに現状では英語のみの対応となっています。

$ src/examples/voice/assistant_library_demo.py
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=965148223698-v9m9nb4bgdp58qk159cghm1ketnvin5t.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fassistant-sdk-prototype&state=tA7PMNhUtTMQdAdj2vRu7IuMAYcUe3&access_type=offline
既存のブラウザ セッションに新しいウィンドウが作成されました。
[2018-01-13 14:57:30,435] INFO:google_auth_oauthlib.flow:"GET /?state=tA7PMNhUtTMQdAdj2vRu7IuMAYcUe3&code=4/ESIoWeN3UiJqensJetZpHruY2DCoPh6KWWigZzEuqSE HTTP/1.1" 200 65
[2018-01-13 14:57:30,812] INFO:root:OAuth credentials initialized: /home/pi/.cache/voice-recognizer/assistant_credentials.json
Say "OK, Google" then speak, or press Ctrl+C to quit...

 続いて、 Google Assistant の gRPC のサンプルプログラムも提供されているので実行してみます。こちらは Wake Word には対応していないので、プログラムを実行した上でスピーカー上部のボタンを押してから話しかけます。こちらもデフォルトでは英語のみの対応となっています。例えば "What's time is it now?" と話しかけると下記のように認識されていることがわかります。

$ src/examples/voice/assistant_grpc_demo.py
/home/pi/AIY-projects-python/src/aiy/_drivers/_led.py:51: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(channel, GPIO.OUT)
Press the button and speak
[2018-01-13 15:07:12,945] INFO:recorder:started recording
Listening...
[2018-01-13 15:07:46,627] INFO:speech:event_type: 1
[2018-01-13 15:07:46,666] INFO:speech:transcript: what's time is it now
You said " what's time is it now "
Press the button and speak

まとめ

 完成品を買ってくるのとはまた違って、自分で組み立てたデバイスがスマートスピーカーとして動くというのは面白いですね。今回はひとまず組み立てとサンプルプログラムの実行まででしたが、色々なデバイスと組み合わせることもできるので、サンプルプログラムをベースに遊んでみたいと思います。

Alexa Voice Service(AVS) のサンプルを Raspberry Pi で動かす

 Alexa Skills Kit(ASK)の実装は以前試してみましたが、 Alexa Voice Service(AVS) はまだ試してみていなかったので、今回は AVS のサンプルアプリを Raspberry Pi で動かしてみました。基本的には下記のチュートリアルの内容をトレースしたものです。

github.com

 Alexa は主に ASK と AVS で構成されていて、 ASK がクラウド上で動作して処理を返すのに対して、 AVS はデバイス上で動作して、入力された音声データの処理と ASK へのリクエストとレスポンスの仲介を行います。

developer.amazon.com

使用デバイス

 今回使用したデバイスは Raspberry Pi 3 Model B と、下記のUSBマイクです。

https://www.amazon.co.jp/gp/product/B01KZPF1U8/

 スピーカーは 3.5mm ステレオジャックの手持ちのものがなかったので、とりあえず手持ちのヘッドホンを使用して試してみました。

 また、 microSDカードも新しい 16GB のものを用意してセットアップしました。

f:id:akanuma-hiroaki:20180110083836j:plain:w300

Raspberry Pi のセットアップ

 まずは Raspberry Pi のセットアップを下記で紹介されている手順に従ってNOOBSを使用して行います。

github.com

 特に難しいことはありませんが、注意点としては、 micro SD カードは購入時のままでも大抵は Mac で読み書きできますが、改めてフォーマットしてから使用しないと Raspberry Pi でインストーラが起動したところからずっと待ち状態のまま進みませんでした。

 あと追加で raspi-config から Wi-Fi アクセスのセットアップと、 GUI 上から SSH と VNC の有効化を行いました。

Amazon Developer アカウントの作成

 まだ Amazon Developer アカウントを作成していない場合は developer.amazon.com から登録します。私は以前 ASK の実装を試した時に作成していたのでそれを使用しました。詳細な登録手順は割愛します。

f:id:akanuma-hiroaki:20180109080720p:plain

製品とセキュリティプロファイルの作成

 Amazon Developer コンソールから AVS を動かす製品(デバイス)の登録とセキュリティプロファイルの作成を行います。手順については下記ページで紹介されています。

Create Security Profile · alexa/alexa-avs-sample-app Wiki · GitHub

 まず製品の登録です。コンソール上部のメニューから ALEXA をクリックし、 Alexa Voice Service の 始める をクリックします。

f:id:akanuma-hiroaki:20180109083329p:plain

 登録済みの製品の一覧ページに遷移しますので、右上の 製品を作成する をクリックします。

f:id:akanuma-hiroaki:20180109083356p:plain

 製品情報の入力フォームが表示されますので、チュートリアルで紹介されている通り、下記の画像のように入力して、 次へ をクリックします。ここで入力している製品ID はあとでサンプルアプリを動かす際に必要になります。

f:id:akanuma-hiroaki:20180109083414p:plain

 Amazon へログインするためのセキュリティプロファイルの選択画面が表示されますので、 プロフィールを新規作成する リンクをクリックします。

f:id:akanuma-hiroaki:20180109083440p:plain

 セキュリティプロファイル名とセキュリティプロファイル記述の入力フォームが開きますので、ここもチュートリアルの内容に従って下記のように入力して、 次へ をクリックします。

f:id:akanuma-hiroaki:20180109083606p:plain

 さらに画面下部にプラットフォーム情報の入力フォームが開きます。ここで表示されているクライアントIDとクライアントシークレットはあとでサンプルアプリを動かす際に必要になりますのでメモしておきます。「許可された出荷地」のフォームにURLを入力して 追加 をクリックするとURLが追加されますので、 http://localhost:3000https://localhost:3000 を追加します。また、「許可された返品 URL」の方にも同様の手順で http://localhost:3000/authresponsehttps://localhost:3000/authresponse を追加し、規約への同意のチェックボックスにチェックを入れて 完了する をクリックすると製品が追加されます。

f:id:akanuma-hiroaki:20180110224830p:plain

 続いて、作成したセキュリティプロファイルを有効化します。 Developer コンソールの Amazon でログイン のページに移動し、プルダウンから先ほど作成したセキュリティプロファイルを選択して Confirm をクリックします。

f:id:akanuma-hiroaki:20180109083937p:plain

 プライバシーポリシーの入力フォームが表示されますので、ダミーのURLを入力しておき、 Save をクリックします。

f:id:akanuma-hiroaki:20180109084000p:plain

サンプルアプリの clone

 GitHub からサンプルアプリを clone します。

$ cd ~/デスクトップ
$ git clone https://github.com/alexa/alexa-avs-sample-app.git
Cloning into 'alexa-avs-sample-app'...
remote: Counting objects: 1488, done.
remote: Total 1488 (delta 0), reused 0 (delta 0), pack-reused 1488
Receiving objects: 100% (1488/1488), 19.94 MiB | 1.57 MiB/s, done.
Resolving deltas: 100% (638/638), done.
Checking connectivity... done.

インストールスクリプトの編集

 インストールスクリプトを編集し、先ほど作成した製品とセキュリティプロファイルの製品ID(ProductID)、クライアントID(ClientID)、クライアントシークレット(ClientSecret)を記載します。

$ cd ~/デスクトップ/alexa-avs-sample-app/
$ vi automated_install.sh
$ head -20 automated_install.sh 
#!/bin/bash

#-------------------------------------------------------
# Paste from developer.amazon.com below
#-------------------------------------------------------

# This is the name given to your device or mobile app in the Amazon developer portal. To look this up, navigate to https://developer.amazon.com/edw/home.html. It may be labeled Device Type ID.
ProductID=my_device

# Retrieve your client ID from the web settings tab within the developer console: https://developer.amazon.com/edw/home.html
ClientID=amzn1.application-oa2-client.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Retrieve your client secret from the web settings tab within the developer console: https://developer.amazon.com/edw/home.html
ClientSecret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

#-------------------------------------------------------
# No need to change anything below this...
#-------------------------------------------------------

#-------------------------------------------------------

インストールスクリプトの実行

 それでは下記のようにインストールスクリプトを実行します。

$ . automated_install.sh

 いくつか質問のプロンプトが表示されますが、yes で回答するとインストールが始まります。

サンプルアプリの実行

 ここまでで一通り準備はできたのでサンプルアプリを実行してみます。サンプルアプリは下記3つの要素で構成されています。

  • Companion Service:サンプルアプリの認証を行うためのWebサービス

  • AVS Sample App:AVSサンプルアプリ本体

  • Wake Word Engine:"Alexa" というフレーズでサンプルアプリとのインタラクションを開始するためのエンジン

 上記をそれぞれ別ターミナルで動作させます。インストールスクリプトの実行までは ssh 等での CLI からの実行でも問題ないかと思いますが、サンプルアプリでは GUI やブラウザを使用しますので、直接 Raspberry Pi にログインするか、 VNC Viewer などで Raspberry Pi の GUI デスクトップにアクセスして実行します。まずは Companion Service を起動します。 Companion Service は node.js のサーバアプリになっています。

$ cd ~/デスクトップ/alexa-avs-sample-app/samples
$ cd companionService && npm start

> alexa-voice-service-sample-companion-service@1.0.0 start /home/pi/デスクトップ/alexa-avs-sample-app/samples/companionService
> node ./bin/www

This node service needs to be running to store token information memory and vend them for the AVS app.

Listening on port 3000

 これでポート 3000 で接続を待ち受けます。続いてサンプルアプリ本体を起動します。Companion Service とは別のターミナルで下記コマンドを実行します。サンプルアプリ本体は Java アプリになっています。

$ cd ~/デスクトップ/alexa-avs-sample-app/samples
$ cd javaclient && mvn exec:exec
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building Alexa Voice Service Sample Java Client 20160207.7
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ sample-java-client ---

 サンプルアプリを起動すると下記のような Window が表示されます。

f:id:akanuma-hiroaki:20180110075434p:plain

 これはデバイスを登録するためにデフォルトブラウザを起動して表示されているURLにアクセスするかどうかということなので、 はい をクリックするとブラウザが起動します。起動しない場合は手動でブラウザを起動して表示されているURLにアクセスします。プライバシー保護の警告が表示されますが、問題ないので 詳細設定 をクリックします。

f:id:akanuma-hiroaki:20180110080033p:plain

 localhost にアクセスする をクリックして続行します。

f:id:akanuma-hiroaki:20180110080137p:plain

 Amazon のログインページに遷移しますのでメールアドレスとパスワードでログインします。

f:id:akanuma-hiroaki:20180110080420p:plain

 認証に成功すると https://localhost:3000/authresponse で始まるURLに遷移し、ブラウザには下記のように表示されます。

device tokens ready これで認証は完了です。 Java アプリの方に戻ると下記のような Window が表示されていますので、 OK をクリックして Window を閉じます。

f:id:akanuma-hiroaki:20180110081115p:plain

 するとサンプルアプリのGUIが表示されます。

f:id:akanuma-hiroaki:20180110081443p:plain

 ここまでで GUI のマイクマークをクリックすることで Alexa とのインタラクションをスタートすることはできるようになっていますが、 Wake Word を使用するために Wake Word Engine を起動します。このサンプルアプリのプロジェクトではサードパーティの Wake Word Engine として Sensory の TrulyHandsFree と KITT.AI の Snowboy がサポートされています。今回は下記のように Sensory の TrulyHandsFree を使用してみます。

$ cd ~/デスクトップ/alexa-avs-sample-app/samples
$ cd wakeWordAgent/src && ./wakeWordAgent -e sensory
INFO:main: Starting Wake Word Agent
INFO:WakeWordAgent: State set to IDLE(2)
INFO:Initializing Sensory library | library name: TrulyHandsfree | library version: 5.0.0-beta.10.2 | model file: ../ext/resources/spot-alexa-rpi.snsr
WARNING:Library expires on: License expires on 28 Mar 2018 00:00:00 GMT
INFO:SensoryWakeWordEngine: mainLoop thread started
INFO:WakeWordIPCSocket::mainLoop thread started
INFO:WakeWordIPCSocket: init socket on port:5123
INFO:WakeWordAgent: thread started
INFO:===> Connected to AVS client <===

動作確認

 それではサンプルアプリの動作を確認してみます。 GUI のマイクマークをクリックするか、 "Alexa" と話しかけるとインタラクションが開始します。「今何時?』「今日の天気は?」などと質問すると Amazon Echo と同じように Alexa が応答してくれます。 Wake Word Engine を動かしているターミナルには下記のような出力があります。

INFO:===> WakeWordAgent: wake word detected <===
INFO:WakeWordAgent: State set to WAKE_WORD_DETECTED(3)
INFO:WakeWordAgent: State set to SENT_WAKE_WORD_DETECTED(4)
INFO:WakeWordAgent: IPC Command received:3
INFO:WakeWordAgent: State set to WAKE_WORD_PAUSE_REQUESTED(5)
INFO:SensoryWakeWordEngine: handling pause
INFO: *** THREAD JOINING: Sensory ***
INFO:SensoryWakeWordEngine: mainLoop thread ended
INFO:WakeWordAgent: State set to WAKE_WORD_PAUSED(6)
INFO:WakeWordAgent: IPC Command received:4
INFO:WakeWordAgent: State set to WAKE_WORD_RESUME_REQUESTED(7)
INFO:SensoryWakeWordEngine: handling resume
INFO:SensoryWakeWordEngine: mainLoop thread started
INFO:WakeWordAgent: State set to IDLE(2)

 ちなみに Wake Word Engine については、 Amazon Echo Dot ではカタカナ発音の「アレクサ」でも十分検知してくれましたが、今回のケースでは英語っぽく "Alexa" と発音しないと検知してくれませんでした。

まとめ

 今回はチュートリアルの内容をトレースすることで Raspberry Pi と Alexa を統合することができましたが、ここまでの内容だけでは Amazon Echo をそのまま使うのと変わらないので、 Raspberry Pi の自由度を活かして色々なデバイスと連携させたりしてみたいと思っています。ただ、サンプルアプリで Java アプリが公開されているものの、実装方法の詳細についての説明は見つからず、 C++ の SDK である AVS Device SDK も見てみましたが詳細な部分はよくわからなかったので、今後わかりそうであれば Raspberry Pi 上で動かす独自アプリを実装してみたいと思います。

github.com

AWS Greengrass のデバイスコードを Python版 SDK で実装してみる

f:id:akanuma-hiroaki:20180105082050p:plain:right

 前回は AWS Greengrass の公式ドキュメントで紹介されているロボットアームのシナリオをそのまま動かしてみましたが、手順をそのままトレースしただけだったので、内容の理解のためにも Python 版の SDK を使ってデバイス用コードを実装してみました。

 具体的には、グループやサブスクリプション等の設定は前回のものをそのまま使用し、 RobotArm_Thing と Switch_Thing で動かすプログラムを、 AWS IoT C++ Device SDK のサンプルとして提供されていたものと同じものを AWS IoT Device SDK for Python を使用して実装しています。

github.com

サブスクリプションの設定内容

 コードの処理内容をイメージしやすくするためにも、コードの中身の前に、サブスクリプションの内容について振り返ってみたいと思います。サブスクリプションの内容が理解できると、各デバイスや Greengrass Core、クラウド上の AWS IoT サービスなどがどのように連携しているかイメージしやすくなるかと思います。前回作成したサブスクリプションの内容は下記の通りですが、それぞれの設定がどういった内容になるのか改めて整理してみます。

$ aws greengrass get-subscription-definition-version --subscription-definition-id 2eb1f6e4-a960-4796-8e0d-e67adbdeabd7 --subscription-definition-version-id c75cc6e2-af57-48fb-8f6a-42e9a5f74d10
{
    "Definition": {
        "Subscriptions": [
            {
                "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
                "Target": "GGShadowService", 
                "Id": "1", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Id": "10", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Id": "11", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/rejected"
            }, 
            {
                "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime", 
                "Id": "2", 
                "Subject": "/topic/state"
            }, 
            {
                "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Target": "GGShadowService", 
                "Id": "3", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Id": "4", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/delta"
            }, 
            {
                "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime", 
                "Target": "cloud", 
                "Id": "5", 
                "Subject": "/topic/metering"
            }, 
            {
                "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage", 
                "Target": "GGShadowService", 
                "Id": "6", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update"
            }, 
            {
                "Source": "cloud", 
                "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage", 
                "Id": "7", 
                "Subject": "/topic/update"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
                "Id": "8", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
                "Id": "9", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/rejected"
            }
        ]
    }, 
    "Version": "c75cc6e2-af57-48fb-8f6a-42e9a5f74d10", 
    "CreationTimestamp": "2017-12-29T13:14:34.243Z", 
    "Id": "2eb1f6e4-a960-4796-8e0d-e67adbdeabd7", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/subscriptions/2eb1f6e4-a960-4796-8e0d-e67adbdeabd7/versions/c75cc6e2-af57-48fb-8f6a-42e9a5f74d10"
}

 上記設定について Id 順に私なりに理解している内容を書いていきたいと思います。

{
    "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
    "Target": "GGShadowService", 
    "Id": "1", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update"
}, 

 Id: 1 は Switch_Thing が RobotArm_Thing の Device Shadow の状態を更新する際の設定です。 Switch_Thing から Greengrass Core デバイス の Subject に示されるトピックに publish されると、 Target である Greengrass の Device Shadow の Service に publish された内容が送信されます。

{
    "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
    "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime", 
    "Id": "2", 
    "Subject": "/topic/state"
}, 

 Id: 2 は RobotArm_Thing が MQTTブローカー(Greengrass Core) に Publish した場合の設定です。 Subject に示されるトピックに publish されると、 Target に指定されている Lambda Function に publish された内容が送信され、 Greengrass Core デバイス上で実行されます。

{
    "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
    "Target": "GGShadowService", 
    "Id": "3", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update"
}, 

 Id: 3 は RobotArm_Thing が自身の Device Shadow の状態を更新する際の設定です。 Source が RobotArm_Thing 自身になったという以外は Id: 1 と同様です。

{
    "Source": "GGShadowService", 
    "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
    "Id": "4", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update/delta"
}, 

 Id: 4 は Greengrass Core の Device Shadow の Service が delta トピックに publish した場合の設定です。 RobotArm_Thing の Device Shadow の desired ステータスが更新され、 reported ステータスと差異が発生するとその差異が Subject に示される delta トピックに Publish され、 Target である RobotArm_Thing に送信されます。

{
    "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime", 
    "Target": "cloud", 
    "Id": "5", 
    "Subject": "/topic/metering"
}, 

 Id: 5 は Greengrass Core デバイス上で稼働する Lambda Function からクラウド上の AWS IoT サービスにメッセージを送信する場合の設定です。 Source に示されている Lambda Function から Subject に示されているトピックに publish すると、 AWS IoT サービスの同様のトピックにその内容が publish されます。

{
    "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage", 
    "Target": "GGShadowService", 
    "Id": "6", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update"
}, 

 Id: 6 は Greengrass Core デバイス上で稼働する Lambda Function から Greengrass Core の Device Shadow のサービスにメッセージを publish する際の設定です。 Source に示されている Lambda Function から RobotArm_Thing の Device Shadow への更新がリクエストされると Subject に示されるトピックに更新用のメッセージが publish され、 Target に示されている Greengrass Core の Device Shadow のサービスへ送信されます。

{
    "Source": "cloud", 
    "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage", 
    "Id": "7", 
    "Subject": "/topic/update"
}, 

 Id: 7 はクラウド上の AWS IoT のコンソールからメッセージを publish した場合の設定です。コンソールから Subject に示されているトピックに publish されると、Target に示されている Greengrass Core デバイス上で稼働している Lambda Function にメッセージが送信されます。

{
    "Source": "GGShadowService", 
    "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
    "Id": "8", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted"
}, 

 Id: 8 は Switch_Thing から RobotArm_Thing の Device Shadow の更新が成功した場合の設定です。 更新が成功すると Source に示されている Greengrass Core の Device Shadow サービスから Subject に示されている accepted トピックにメッセージが publish され、その内容が Switch_Thing に送信されます。

{
    "Source": "GGShadowService", 
    "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
    "Id": "9", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update/rejected"
}

 Id: 9 は Id: 8 とは逆に Device Shadow の更新が失敗した場合の設定です。更新が失敗すると Source に示されている Greengrass Core の Device Shadow サービスから Subject に示されている rejected トピックにメッセージが publish され、その内容が Switch_Thing に送信されます。

{
    "Source": "GGShadowService", 
    "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
    "Id": "10", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted"
}, 

 Id: 10 は Id: 8 と同様に RobotArm_Thing の Device Shadow の更新に成功した場合の設定ですが、送信先を RobotArm_Thing にしています。

{
    "Source": "GGShadowService", 
    "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
    "Id": "11", 
    "Subject": "$aws/things/RobotArm_Thing/shadow/update/rejected"
}, 

 Id: 11 は Id: 9 と同様に RobotArm_Thing の Device Shadow の更新に失敗した場合の設定ですが、送信先を RobotArm_Thing にしています。

RobotArm_Thing のデバイスコード

 デバイスコードで実行している処理の概要は、下記のような流れになります。

  • DiscoveryInfoProvider で自身が属する Greengrass Group の Core デバイスの情報を取得する

  • Greengrass Core に接続する

  • Device Shadow や MQTTクライアントのインスタンスを取得する

  • Device Shadow の更新や MQTTブローカーへのメッセージの publish など任意の処理を行う

 上記の流れは RobotArm_Thing でも Switch_Thing でも同様です。まずは RobotArm_Thing のデバイスコードから見ていきます。ひとまずコード全体を掲載します。わかりやすくするためにエラーハンドリングなどの処理は省略しています。

import time
import uuid
import json
import logging
from AWSIoTPythonSDK.core.greengrass.discovery.providers import DiscoveryInfoProvider
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTShadowClient
from AWSIoTPythonSDK.exception.AWSIoTExceptions import subscribeQueueDisabledException

class RobotArm:
    ENDPOINT = 'greengrass.iot.ap-northeast-1.amazonaws.com'
    ROOT_CA_PATH = 'certs/robotArm/root-ca.pem'
    CERTIFICATE_PATH = 'certs/robotArm/c5e6d39f7b-certificate.pem.crt'
    PRIVATE_KEY_PATH = 'certs/robotArm/c5e6d39f7b-private.pem.key'
    THING_NAME = 'RobotArm_Thing'
    CLIENT_ID = 'RobotArm_Thing'
    GROUP_CA_PATH = './groupCA/'
    METERING_TOPIC = '/topic/state'

    def __init__(self):
        self.discoveryInfoProvider = DiscoveryInfoProvider()
        self.discoveryInfoProvider.configureEndpoint(self.ENDPOINT)
        self.discoveryInfoProvider.configureCredentials(self.ROOT_CA_PATH, self.CERTIFICATE_PATH, self.PRIVATE_KEY_PATH)
        self.discoveryInfoProvider.configureTimeout(10)

        logger = logging.getLogger('AWSIoTPythonSDK.core')
        logger.setLevel(logging.DEBUG)
        streamHandler = logging.StreamHandler()
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        streamHandler.setFormatter(formatter)
        logger.addHandler(streamHandler)

    def get_ggc_info(self):
        discoveryInfo = self.discoveryInfoProvider.discover(self.THING_NAME)
        caList = discoveryInfo.getAllCas()
        coreList = discoveryInfo.getAllCores()

        groupId, ca = caList[0]
        coreInfo = coreList[0]
        return (groupId, ca, coreInfo)

    def write_ca_file(self, groupId, ca):
        groupCAPath = self.GROUP_CA_PATH + groupId + '_CA_' + str(uuid.uuid4()) + '.crt'
        groupCAFile = open(groupCAPath, 'w')
        groupCAFile.write(ca)
        groupCAFile.close()
        return groupCAPath

    def connect_to_shadow_service(self, groupCAPath, coreInfo):
        shadowClient = AWSIoTMQTTShadowClient(self.CLIENT_ID)
        shadowClient.configureCredentials(groupCAPath, self.PRIVATE_KEY_PATH, self.CERTIFICATE_PATH)

        connectivityInfo = coreInfo.connectivityInfoList[0]
        ggcHost = connectivityInfo.host
        ggcPort = connectivityInfo.port

        shadowClient.configureEndpoint(ggcHost, ggcPort)
        shadowClient.connect()
        return shadowClient

    def get_mqtt_client(self, shadowClient):
        return shadowClient.getMQTTConnection()

    def get_device_shadow(self, shadowClient):
        return shadowClient.createShadowHandlerWithName(self.CLIENT_ID, True)

    def shadow_update_callback(self, payload, responseStatus, token):
        reportedState = json.loads(payload)['state']['reported']['myState']
        self.publish_mqtt_async(self.mqttClient, reportedState)

    def shadow_delta_callback(self, payload, responseStatus, token):
        desiredState = json.loads(payload)['state']['myState']
        self.publish_shadow_state(self.deviceShadow, desiredState)

    def publish_shadow_state(self, deviceShadow, state):
        reportedState = { 'state': { 'reported': { 'myState': state } } }
        print('Sending State -------\n%s' % reportedState)
        deviceShadow.shadowUpdate(json.dumps(reportedState), self.shadow_update_callback, 5)

    def publish_mqtt_async(self, mqttClient, state):
        payload = { 'state': state }
        mqttClient.publish(self.METERING_TOPIC, json.dumps(payload), 0)

    def wait_for_update_shadow(self, deviceShadow):
        deviceShadow.shadowRegisterDeltaCallback(self.shadow_delta_callback)

    def execute(self):
        groupId, ca, coreInfo = self.get_ggc_info()
        groupCAPath = self.write_ca_file(groupId, ca)

        shadowClient = self.connect_to_shadow_service(groupCAPath, coreInfo)

        self.deviceShadow = self.get_device_shadow(shadowClient)
        self.mqttClient = self.get_mqtt_client(shadowClient)

        self.publish_shadow_state(self.deviceShadow, 'off')

        self.wait_for_update_shadow(self.deviceShadow)
        print('Waiting for an update!')

        while True:
            time.sleep(1)

if '__main__' == __name__:
    robotArm = RobotArm()
    robotArm.execute()

 各処理について解説していきます。まずはコンストラクタで所属している Greengrass Group の情報を取得するための DiscoveryInfoProvider のインスタンスを取得して設定を行なっています。エンドポイントについてはアカウント固有の AWS IoT のエンドポイントではなく、リージョンごとに固定のエンドポイントで良いようです。

    def __init__(self):
        self.discoveryInfoProvider = DiscoveryInfoProvider()
        self.discoveryInfoProvider.configureEndpoint(self.ENDPOINT)
        self.discoveryInfoProvider.configureCredentials(self.ROOT_CA_PATH, self.CERTIFICATE_PATH, self.PRIVATE_KEY_PATH)
        self.discoveryInfoProvider.configureTimeout(10)

 そしてその DiscoveryInfoProvider インスタンスを使用して自身の Thing Name を引数にして discover メソッドを実行して Greengrass Group の情報を取得します。

    def get_ggc_info(self):
        discoveryInfo = self.discoveryInfoProvider.discover(self.THING_NAME)
        caList = discoveryInfo.getAllCas()
        coreList = discoveryInfo.getAllCores()

        groupId, ca = caList[0]
        coreInfo = coreList[0]
        return (groupId, ca, coreInfo)

 デバイスから Greengrass Core に接続するには証明書の情報が必要なので Greengrass Group の情報から取得できる証明書を保存しておきます。

    def write_ca_file(self, groupId, ca):
        groupCAPath = self.GROUP_CA_PATH + groupId + '_CA_' + str(uuid.uuid4()) + '.crt'
        groupCAFile = open(groupCAPath, 'w')
        groupCAFile.write(ca)
        groupCAFile.close()
        return groupCAPath

 そして Greengrass Core の Device Shadow サービスに接続し、 AWSIoTMQTTShadowClient のインスタンスを返します。

    def connect_to_shadow_service(self, groupCAPath, coreInfo):
        shadowClient = AWSIoTMQTTShadowClient(self.CLIENT_ID)
        shadowClient.configureCredentials(groupCAPath, self.PRIVATE_KEY_PATH, self.CERTIFICATE_PATH)

        connectivityInfo = coreInfo.connectivityInfoList[0]
        ggcHost = connectivityInfo.host
        ggcPort = connectivityInfo.port

        shadowClient.configureEndpoint(ggcHost, ggcPort)
        shadowClient.connect()
        return shadowClient

 そのインスタンスから、実際に Device Shadow を操作するために、自身の Thing Name を CLIENT_ID として deviceShadow のインスタンスを取得します。

    def get_device_shadow(self, shadowClient):
        return shadowClient.createShadowHandlerWithName(self.CLIENT_ID, True)

 さらに同じ接続を使用してプレーンな MQTT の操作を行うために AWSIoTMQTTShadowClient のインスタンスから AWSIoTMQTTClient のインスタンスを取得します。

    def get_mqtt_client(self, shadowClient):
        return shadowClient.getMQTTConnection()

 そして起動時にはまず状態を off として Device Shadow の reported を更新します。また、コールバックメソッドで現在の状態を MQTT トピックに publish しています。今回は responseStatus に関係なく処理を行っていますが、本来は responseStatus によって処理をハンドリングする必要があるかと思います。

    def publish_shadow_state(self, deviceShadow, state):
        reportedState = { 'state': { 'reported': { 'myState': state } } }
        print('Sending State -------\n%s' % reportedState)
        deviceShadow.shadowUpdate(json.dumps(reportedState), self.shadow_update_callback, 5)

    def shadow_update_callback(self, payload, responseStatus, token):
        reportedState = json.loads(payload)['state']['reported']['myState']
        self.publish_mqtt_async(self.mqttClient, reportedState)

    def publish_mqtt_async(self, mqttClient, state):
        payload = { 'state': state }
        mqttClient.publish(self.METERING_TOPIC, json.dumps(payload), 0)

 最後に Switch_Thing からの Device Shadow の更新を待ち受けるため、 delta トピックにメッセージが publish された時のコールバックメソッドを登録します。今回の処理は先ほどの publish_shadow_state を使用して、 reported のステータスを更新し、 desired ステータスに揃えているだけですが、実際はハードのスイッチの ON/OFF の処理などを行うことになるかと思います。

    def wait_for_update_shadow(self, deviceShadow):
        deviceShadow.shadowRegisterDeltaCallback(self.shadow_delta_callback)

    def shadow_delta_callback(self, payload, responseStatus, token):
        desiredState = json.loads(payload)['state']['myState']
        self.publish_shadow_state(self.deviceShadow, desiredState)

 このスクリプトを実行すると、デバッグ用の出力を除くと下記のような出力があり、 Switch_Thing からの Device Shadow の更新を待ち受けます。

$ python robot_arm.py 
Sending State -------
{'state': {'reported': {'myState': 'off'}}}
Waiting for an update!

Switch_Thing のデバイスコード

 続いて Switch_Thing のデバイスコードです。こちらもまずはコード全体を掲載します。エラーハンドリング等の処理は省略しています。

import sys
import time
import uuid
import json
import logging
from AWSIoTPythonSDK.core.greengrass.discovery.providers import DiscoveryInfoProvider
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTShadowClient

class Switch:
    ENDPOINT = 'greengrass.iot.ap-northeast-1.amazonaws.com'
    ROOT_CA_PATH = 'certs/switch/root-ca.pem'
    CERTIFICATE_PATH = 'certs/switch/a20b621e05-certificate.pem.crt'
    PRIVATE_KEY_PATH = 'certs/switch/a20b621e05-private.pem.key'
    THING_NAME = 'Switch_Thing'
    CLIENT_ID = 'Switch_Thing'
    TARGET_THING_NAME = 'RobotArm_Thing'
    GROUP_CA_PATH = './groupCA/'

    def __init__(self):
        self.discoveryInfoProvider = DiscoveryInfoProvider()
        self.discoveryInfoProvider.configureEndpoint(self.ENDPOINT)
        self.discoveryInfoProvider.configureCredentials(self.ROOT_CA_PATH, self.CERTIFICATE_PATH, self.PRIVATE_KEY_PATH)
        self.discoveryInfoProvider.configureTimeout(10)

        logger = logging.getLogger('AWSIoTPythonSDK.core')
        logger.setLevel(logging.INFO)
        streamHandler = logging.StreamHandler()
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        streamHandler.setFormatter(formatter)
        logger.addHandler(streamHandler)

    def get_ggc_info(self):
        discoveryInfo = self.discoveryInfoProvider.discover(self.THING_NAME)
        caList = discoveryInfo.getAllCas()
        coreList = discoveryInfo.getAllCores()

        groupId, ca = caList[0]
        coreInfo = coreList[0]
        return (groupId, ca, coreInfo)

    def write_ca_file(self, groupId, ca):
        groupCAPath = self.GROUP_CA_PATH + groupId + '_CA_' + str(uuid.uuid4()) + '.crt'
        groupCAFile = open(groupCAPath, 'w')
        groupCAFile.write(ca)
        groupCAFile.close()
        return groupCAPath

    def connect_to_shadow_service(self, groupCAPath, coreInfo):
        shadowClient = AWSIoTMQTTShadowClient(self.CLIENT_ID)
        shadowClient.configureCredentials(groupCAPath, self.PRIVATE_KEY_PATH, self.CERTIFICATE_PATH)

        connectivityInfo = coreInfo.connectivityInfoList[0]
        ggcHost = connectivityInfo.host
        ggcPort = connectivityInfo.port

        shadowClient.configureEndpoint(ggcHost, ggcPort)
        shadowClient.connect()
        return shadowClient

    def get_mqtt_client(self, shadowClient):
        return shadowClient.getMQTTConnection()

    def update_target_device_shadow(self, mqttClient, state):
        update_topic = '$aws/things/%s/shadow/update' % self.TARGET_THING_NAME
        desiredState = { 'state': { 'desired': { 'myState': state } } }
        print('Sending State -------\n%s' % desiredState)
        mqttClient.publish(update_topic, json.dumps(desiredState), 0)

    def execute(self):
        groupId, ca, coreInfo = self.get_ggc_info()
        groupCAPath = self.write_ca_file(groupId, ca)

        shadowClient = self.connect_to_shadow_service(groupCAPath, coreInfo)

        mqttClient = self.get_mqtt_client(shadowClient)

        while True:
            sys.stdout.write('Please enter 1 (turn on) or 0 (turn off) to control the robot arm, q to quit: ')
            user_input = raw_input('')

            if user_input == 'q':
                break

            if user_input == '1':
                state = 'on'
            elif user_input == '0':
                state = 'off'
            else:
                print('Invalid input.')
                continue

            self.update_target_device_shadow(mqttClient, state)

if '__main__' == __name__:
    switch = Switch()
    switch.execute()

 RobotArm_Thing のデバイスコードと違って自身の Device Shadow を扱う必要がないので deviceShadow インスタンスは取得していませんが、それ以外は AWSIoTMQTTClient の取得までは同様の処理になります。

 while ループの中ではユーザからの入力を待ち受け、 on(1) か off(0) の入力があると RobotArm_Thing の Device Shadow の update トピックに desired ステータスを publish します。

    def update_target_device_shadow(self, mqttClient, state):
        update_topic = '$aws/things/%s/shadow/update' % self.TARGET_THING_NAME
        desiredState = { 'state': { 'desired': { 'myState': state } } }
        print('Sending State -------\n%s' % desiredState)
        mqttClient.publish(update_topic, json.dumps(desiredState), 0)

 スクリプトを実行すると、デバッグ用の出力以外では下記のような形で入力を待ち受け、入力があると RobotArm_Thing の Device Shadow が更新されます。

$ python switch.py 
Please enter 1 (turn on) or 0 (turn off) to control the robot arm, q to quit: 1
Sending State -------
{'state': {'desired': {'myState': 'on'}}}

 これで前回実行したロボットアームのシナリオと同様の動作をする処理を Python SDK で実装することができました。

まとめ

 デバイスコードの処理内容は一度わかってしまえば複雑ではないですし、 Python だとよりシンプルにかけて良い感じでしたが、実際はエラーハンドリングの処理を随所に入れていく必要があるかと思いますので、ネットワークの状態など様々な状態を考慮したエラーハンドリングは結構大変になりそうな気がします。また、各デバイスも起動時には自身が所属する Greengass Group の情報を取得するためにはクラウドにアクセスする必要がありますので、完全にローカルのみで動作させるというわけには行かなそうです。 Core デバイスの IP と Port を決め打ちにしてしまえば起動時からローカルのみでも行けそうではありますが、それだと逆にクラウドからは接続情報などを反映させることができなくなるので、微妙なところではあります。

 今回のコードはこちらにも公開しました。

github.com

AWS Greengrass のデバイス連携のシナリオを試す

 前回 Greengrass Core を単体で Raspberry Pi で動かすチュートリアルを試しましたが、やはり複数デバイスを連携させてこそ Greengrass のメリットは大きいと思いますので、公式ドキュメントで公開されているロボットアームのシナリオの例を試してみたいと思います。

docs.aws.amazon.com

Thing の準備

 このシナリオでは AWS IoT の Thing を3つ使いますので、AWS IoT の管理コンソールから下記の名前で Thing を用意しておきます。(Thingの作成方法はここでは割愛させていただきます)

  • GGC_Thing:Greengrass Core 用
  • RobotArm_Thing:RobotArm 用
  • Switch_Thing:RobotArm のスイッチ用

AWS Greengrass Group の作成

 前回のチュートリアルの例ではWebのコンソールから各コンポーネントを作成しましたが、今回のシナリオでは AWS CLI から操作していきます。まずは Greengrass Group の作成です。 aws greengrass create-group コマンドで作成できます。

$ aws greengrass create-group --name "RobotArm_Group"
{
    "Name": "RobotArm_Group", 
    "LastUpdatedTimestamp": "2017-12-29T10:57:26.251Z", 
    "CreationTimestamp": "2017-12-29T10:57:26.251Z", 
    "Id": "c4768328-4dce-4e41-9fd2-3ce8e943072e", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/groups/c4768328-4dce-4e41-9fd2-3ce8e943072e"
}

 作成された Greengrass Group の情報は aws greengrass get-group コマンドで参照できます。

$ aws greengrass get-group --group-id c4768328-4dce-4e41-9fd2-3ce8e943072e
{
    "Name": "RobotArm_Group", 
    "LastUpdatedTimestamp": "2017-12-29T10:57:26.251Z", 
    "CreationTimestamp": "2017-12-29T10:57:26.251Z", 
    "Id": "c4768328-4dce-4e41-9fd2-3ce8e943072e", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/groups/c4768328-4dce-4e41-9fd2-3ce8e943072e"
}

Greengrass Core 定義の作成

 Greengrass Core とその設定情報のリストである Greengrass Core 定義を作成します。 Greengrass のコンポーネントの多くは、まず 定義 を作成して、実際の Thing の情報などを含む バージョン を作成するという流れのようなので、まずは定義自体を作成します。

$ aws greengrass create-core-definition --name "RobotArm_CoreDefinition"
{
    "Name": "RobotArm_CoreDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T11:03:41.101Z", 
    "CreationTimestamp": "2017-12-29T11:03:41.101Z", 
    "Id": "5272397b-ec69-4259-bb5c-6043daade271", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/cores/5272397b-ec69-4259-bb5c-6043daade271"
}

 作成した Greengrass Core 定義は下記のように参照できます。

$ aws greengrass get-core-definition --core-definition-id 5272397b-ec69-4259-bb5c-6043daade271
{
    "Name": "RobotArm_CoreDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T11:03:41.101Z", 
    "CreationTimestamp": "2017-12-29T11:03:41.101Z", 
    "Id": "5272397b-ec69-4259-bb5c-6043daade271", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/cores/5272397b-ec69-4259-bb5c-6043daade271"
}

 続いて Greengrass Core 定義のバージョンを作成します。 --core-definition-id には先ほど作成した Greengrass Core 定義の ID を指定します。また、 --cores には Greengrass Core の Thing の情報を指定します。 json でリストを指定できるようになっていますが、現状では一つの Greengrass Core 定義に指定できる Core デバイスは一つです。 CertificateArn は Thing の証明書の arn を、 ThingArn には Thing の arn を指定します。また、 Id は任意の内容を指定できるようなので、ここでは 1 を設定しておきます。 SyncShadow は Thing の Shadow を同期するかどうかの設定で、 true を指定しておきます。

$ aws greengrass create-core-definition-version --core-definition-id "5272397b-ec69-4259-bb5c-6043daade271" --cores '[{
    "CertificateArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:cert/bdd9d49f5218a3c647f40f63a944887b12c0c46503904338fb4bbbd6f3e19d4d",
    "Id": "1",
    "SyncShadow": true,
    "ThingArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/GGC_Thing"
}]'
{
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/cores/5272397b-ec69-4259-bb5c-6043daade271/versions/c72d9131-69ab-4be5-9123-2ee9709d193b", 
    "Version": "c72d9131-69ab-4be5-9123-2ee9709d193b", 
    "CreationTimestamp": "2017-12-29T11:09:08.632Z", 
    "Id": "5272397b-ec69-4259-bb5c-6043daade271"
}

 作成された Greengrass Core 定義のバージョン情報は下記のように参照できます。

$ aws greengrass get-core-definition-version --core-definition-id 5272397b-ec69-4259-bb5c-6043daade271 --core-definition-version-id c72d9131-69ab-4be5-9123-2ee9709d193b
{
    "Definition": {
        "Cores": [
            {
                "CertificateArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:cert/bdd9d49f5218a3c647f40f63a944887b12c0c46503904338fb4bbbd6f3e19d4d", 
                "ThingArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/GGC_Thing", 
                "SyncShadow": true, 
                "Id": "1"
            }
        ]
    }, 
    "Version": "c72d9131-69ab-4be5-9123-2ee9709d193b", 
    "CreationTimestamp": "2017-12-29T11:09:08.632Z", 
    "Id": "5272397b-ec69-4259-bb5c-6043daade271", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/cores/5272397b-ec69-4259-bb5c-6043daade271/versions/c72d9131-69ab-4be5-9123-2ee9709d193b"
}

デバイス定義の作成

 Greengrass Core と連携させるデバイスの Thing と設定データのリストであるデバイス定義を作成します。デバイス定義も Greengrass Core 定義と同じように、定義自体を作成した上でバージョンを作成します。まずは定義の作成です。

$ aws greengrass create-device-definition --name "RobotArm_DeviceDefinition"
{
    "Name": "RobotArm_DeviceDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T11:22:45.786Z", 
    "CreationTimestamp": "2017-12-29T11:22:45.786Z", 
    "Id": "081b65a9-e057-4395-80f0-d455f224eb23", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/devices/081b65a9-e057-4395-80f0-d455f224eb23"
}

 作成されたデバイス定義は下記のように参照できます。

$ aws greengrass get-device-definition --device-definition-id 081b65a9-e057-4395-80f0-d455f224eb23
{
    "Name": "RobotArm_DeviceDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T11:22:45.786Z", 
    "CreationTimestamp": "2017-12-29T11:22:45.786Z", 
    "Id": "081b65a9-e057-4395-80f0-d455f224eb23", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/devices/081b65a9-e057-4395-80f0-d455f224eb23"
}

 続いてデバイス定義のバージョンを作成します。 --device-definition-id には先ほど作成したデバイス定義の ID を指定します。 --devices でデバイスの Thing のリストを指定します。このシナリオでは前述のように RobotArm_Thing と Switch_Thing を使用します。 CertificateArnThingArn はそれぞれのデバイスの証明書の arn とデバイスの arn を指定します。 Id は任意なので連番で振っておきます。 SyncShadow は Thing の Shadow を同期するかの設定で、 Switch の方では必要ないので false にしておきます。

$ aws greengrass create-device-definition-version --device-definition-id "081b65a9-e057-4395-80f0-d455f224eb23" --devices '[
    {
        "Id": "1",
        "CertificateArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:cert/c5e6d39f7bd79018296f63900f9f36e13d05649ead14d20d7f5ab4de3a812175",
        "SyncShadow": true,
        "ThingArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing"
    },
    {
        "Id": "2",
        "CertificateArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:cert/a20b621e056d59f071cd3eb907d574a90b2dd88c03098caa36b8ea120cbfcb33",
        "SyncShadow": false,
        "ThingArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing"
    }
]'
{
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/devices/081b65a9-e057-4395-80f0-d455f224eb23/versions/4a19989a-7fa7-4322-8425-f8570f625758", 
    "Version": "4a19989a-7fa7-4322-8425-f8570f625758", 
    "CreationTimestamp": "2017-12-29T11:26:30.554Z", 
    "Id": "081b65a9-e057-4395-80f0-d455f224eb23"
}

 作成されたデバイス定義のバージョン情報は下記のように参照できます。

$ aws greengrass get-device-definition-version --device-definition-id 081b65a9-e057-4395-80f0-d455f224eb23 --device-definition-version-id 4a19989a-7fa7-4322-8425-f8570f625758
{
    "Definition": {
        "Devices": [
            {
                "CertificateArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:cert/c5e6d39f7bd79018296f63900f9f36e13d05649ead14d20d7f5ab4de3a812175", 
                "ThingArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "SyncShadow": true, 
                "Id": "1"
            }, 
            {
                "CertificateArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:cert/a20b621e056d59f071cd3eb907d574a90b2dd88c03098caa36b8ea120cbfcb33", 
                "ThingArn": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
                "SyncShadow": false, 
                "Id": "2"
            }
        ]
    }, 
    "Version": "4a19989a-7fa7-4322-8425-f8570f625758", 
    "CreationTimestamp": "2017-12-29T11:26:30.554Z", 
    "Id": "081b65a9-e057-4395-80f0-d455f224eb23", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/devices/081b65a9-e057-4395-80f0-d455f224eb23/versions/4a19989a-7fa7-4322-8425-f8570f625758"
}

Thing のポリシー変更

 Thing 作成時には証明書を作成してポリシーをアタッチしますが、各デバイスがどこまでの操作をできるかというのはそのポリシーの内容によって決まります。今回はひとまず AWS IoT や Greengrass の全ての操作が行えるように、 AWS IoT の管理コンソールから、 GGC_Thing、 RobotArm_Thing、 Switch_Thing にアタッチされているポリシーを下記のように変更しておきます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:*",
        "greengrass:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Lambda Function の用意

 デバイスで動作させる Lambda Function を用意します。このシナリオでは AWS Greengrass Core SDK の一部として提供されている2つの Lambda Function を使用します。 SDK は AWS IoT コンソールからダウンロードできます。コンソールの左メニューの ソフトウェア メニューを選択し、 AWS Greengrass Core SDK の ダウンロードの設定 をクリックします。

f:id:akanuma-hiroaki:20171229211500p:plain

 Python 以外に Node.js や Java の SDK も取得できますが、今回は Python 版を選択して、 Greengrass Core SDK のダウンロード をクリックします。

f:id:akanuma-hiroaki:20171229211507p:plain

 ダウンロードした圧縮ファイルを解凍し、 examples/Storyline というディレクトリの中にある2つの zip ファイルを使用します。 AWS Lambda コンソールから関数を作成します。詳細は割愛しますが、 uptimeLambda という関数名で storyline_uptimeLambda.zip を登録し、 Handler には uptimeLambda.uptime_handler を指定します。登録できたら Actions メニューの 新しいバージョンを発行 をクリックしてバージョンを発行しておきます。そしてあとで次のバージョンを発行した時に Greengrass 側での参照先を変更しなくても良いように、エイリアスを作成しておきます。 Actions メニューの エイリアスの作成 をクリックします。

f:id:akanuma-hiroaki:20171230005152p:plain

 エイリアス名に storyLineUptime と入力し、バージョンには先ほど発行したバージョンを選択して 作成 をクリックします。

f:id:akanuma-hiroaki:20171229213856p:plain

 これでひとまず uptimeLambda 関数は登録できたので、次に messageLambda という関数名で storyline_messageLambda.zip を登録し、 Handler には messageLambda.message_handler を指定します。バージョンの発行も同様に行い、エイリアスも storyLineMessage という名前で作成しておきます。

Lambda Function 定義の作成

 次にデバイス上で動作させる Lambda Function の定義を作成します。こちらも定義自体を作成した後にバージョンを作成します。まずは定義自体の作成です。

$ aws greengrass create-function-definition --name "RobotArm_FunctionDefinition"
{
    "Name": "RobotArm_FunctionDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T11:48:57.249Z", 
    "CreationTimestamp": "2017-12-29T11:48:57.249Z", 
    "Id": "1ed0c6f7-c34e-4ed1-a8d4-740f99d62778", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/functions/1ed0c6f7-c34e-4ed1-a8d4-740f99d62778"
}

 作成された定義情報は下記のように参照できます。

$ aws greengrass get-function-definition --function-definition-id 1ed0c6f7-c34e-4ed1-a8d4-740f99d62778
{
    "Name": "RobotArm_FunctionDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T11:48:57.249Z", 
    "CreationTimestamp": "2017-12-29T11:48:57.249Z", 
    "Id": "1ed0c6f7-c34e-4ed1-a8d4-740f99d62778", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/functions/1ed0c6f7-c34e-4ed1-a8d4-740f99d62778"
}

 続いて Lambda Function 定義のバージョンを作成します。 --function-definition-id には先ほど作成した Lambda Function 定義の ID を指定します。 --functions で使用する Lambda Function のリストを指定します。 FunctionArn には先ほど登録した Lambda Function のエイリアスの arn を指定します。AWS Lambda のコンソールでエイリアスの arn を確認するには、 限定条件 メニューから該当のエイリアスを選択します。

f:id:akanuma-hiroaki:20171230005324p:plain

 該当のエイリアスの情報が表示されますので、右上にある ARN がエイリアスの ARN になります。

f:id:akanuma-hiroaki:20171230005343p:plain

 Executable には Lambda Function のハンドラを指定し、 MemorySizeTimeout はここではそれぞれ 128000 と 3 にしておきます。 Id には任意の値を指定できるので、ここではそれぞれ uptime-lambda と message-lambda としておきます。

$ aws greengrass create-function-definition-version --function-definition-id "1ed0c6f7-c34e-4ed1-a8d4-740f99d62778" --functions '[
{
    "Id": "uptime-lambda",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime",
    "FunctionConfiguration": {
        "Executable": "uptimeLambda.uptime_handler",
        "MemorySize": 128000,
        "Timeout": 3
    }
},
{
    "Id": "message-lambda",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage",
    "FunctionConfiguration": {
        "Executable": "messageLambda.message_handler",
        "MemorySize": 128000,
        "Timeout": 3
    }
}]'
{
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/functions/1ed0c6f7-c34e-4ed1-a8d4-740f99d62778/versions/a203e6af-74b8-4a52-baf4-10c648e0f3fb", 
    "Version": "a203e6af-74b8-4a52-baf4-10c648e0f3fb", 
    "CreationTimestamp": "2017-12-29T12:57:05.869Z", 
    "Id": "1ed0c6f7-c34e-4ed1-a8d4-740f99d62778"
}

 作成されたバージョン情報は下記のように確認できます。

$ aws greengrass get-function-definition-version --function-definition-id 1ed0c6f7-c34e-4ed1-a8d4-740f99d62778 --function-definition-version-id a203e6af-74b8-4a52-baf4-10c648e0f3fb
{
    "Definition": {
        "Functions": [
            {
                "FunctionConfiguration": {
                    "Executable": "messageLambda.message_handler", 
                    "MemorySize": 128000, 
                    "Timeout": 3
                }, 
                "Id": "message-lambda", 
                "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage"
            }, 
            {
                "FunctionConfiguration": {
                    "Executable": "uptimeLambda.uptime_handler", 
                    "MemorySize": 128000, 
                    "Timeout": 3
                }, 
                "Id": "uptime-lambda", 
                "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime"
            }
        ]
    }, 
    "Version": "a203e6af-74b8-4a52-baf4-10c648e0f3fb", 
    "CreationTimestamp": "2017-12-29T12:57:05.869Z", 
    "Id": "1ed0c6f7-c34e-4ed1-a8d4-740f99d62778", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/functions/1ed0c6f7-c34e-4ed1-a8d4-740f99d62778/versions/a203e6af-74b8-4a52-baf4-10c648e0f3fb"
}

サブスクリプション定義の作成

 AWS Greengrass Group 内でのメッセージの送受信方法を指定するためのサブスクリプションの定義を作成します。まずは定義自体の作成です。

$ aws greengrass create-subscription-definition --name "RobotArm_SubscriptionDefinition"
{
    "Name": "RobotArm_SubscriptionDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T13:07:19.866Z", 
    "CreationTimestamp": "2017-12-29T13:07:19.866Z", 
    "Id": "2eb1f6e4-a960-4796-8e0d-e67adbdeabd7", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/subscriptions/2eb1f6e4-a960-4796-8e0d-e67adbdeabd7"
}

 作成された定義情報は下記のように参照可能です。

$ aws greengrass get-subscription-definition --subscription-definition-id 2eb1f6e4-a960-4796-8e0d-e67adbdeabd7
{
    "Name": "RobotArm_SubscriptionDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T13:07:19.866Z", 
    "CreationTimestamp": "2017-12-29T13:07:19.866Z", 
    "Id": "2eb1f6e4-a960-4796-8e0d-e67adbdeabd7", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/subscriptions/2eb1f6e4-a960-4796-8e0d-e67adbdeabd7"
}

 続けてサブスクリプション定義のバージョンを作成します。 --subscription-definition-id には先ほど作成したサブスクリプション定義の ID を指定します。 --subscriptions にはサブスクリプションの内容を json で指定します。 Id は任意の内容を指定できるのでここでは連番を振っておきます。 Source にはメッセージの送信元、 Target にはメッセージの受取先、 Subject にはメッセージをフィルタするために対象の MQTT トピックを指定しておきます。 Shadow のアップデートや Lambda Function の実行など、使用する全てのパターンについて指定が必要なので長くなりますが、下記のようにコマンドを実行します。

$ aws greengrass create-subscription-definition-version --subscription-definition-id "2eb1f6e4-a960-4796-8e0d-e67adbdeabd7" --subscriptions '[
    {
        "Id": "1",
        "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing",
        "Subject": "$aws/things/RobotArm_Thing/shadow/update",
        "Target": "GGShadowService"
    },
    {
        "Id": "2",
        "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
        "Subject": "/topic/state", 
        "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime"
    },
    {
        "Id": "3",
        "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing",
        "Subject": "$aws/things/RobotArm_Thing/shadow/update",
        "Target": "GGShadowService"
    },
    {
        "Id": "4",
        "Source": "GGShadowService",
        "Subject": "$aws/things/RobotArm_Thing/shadow/update/delta",
        "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing"
    },
    {
        "Id": "5",
        "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime",
        "Subject": "/topic/metering",
        "Target": "cloud"
    },
    {
        "Id": "6",
        "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage",
        "Subject": "$aws/things/RobotArm_Thing/shadow/update",
        "Target": "GGShadowService"
    },
    {
        "Id": "7",
        "Source": "cloud",
        "Subject": "/topic/update",
        "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage"
    },
    {
        "Id": "8",
        "Source": "GGShadowService",
        "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted",
        "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing"
    },
    {
        "Id":"9",
        "Source":"GGShadowService",
        "Subject":"$aws/things/RobotArm_Thing/shadow/update/rejected",
        "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing"
    },
    {
        "Id":"10",
        "Source":"GGShadowService",
        "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted",
        "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing"
    },
    {
        "Id":"11",
        "Source":"GGShadowService",
        "Subject": "$aws/things/RobotArm_Thing/shadow/update/rejected",
        "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing"
    }
]'
{
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/subscriptions/2eb1f6e4-a960-4796-8e0d-e67adbdeabd7/versions/c75cc6e2-af57-48fb-8f6a-42e9a5f74d10", 
    "Version": "c75cc6e2-af57-48fb-8f6a-42e9a5f74d10", 
    "CreationTimestamp": "2017-12-29T13:14:34.243Z", 
    "Id": "2eb1f6e4-a960-4796-8e0d-e67adbdeabd7"
}

 作成されたバージョン情報は下記のように参照できます。

$ aws greengrass get-subscription-definition-version --subscription-definition-id 2eb1f6e4-a960-4796-8e0d-e67adbdeabd7 --subscription-definition-version-id c75cc6e2-af57-48fb-8f6a-42e9a5f74d10
{
    "Definition": {
        "Subscriptions": [
            {
                "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
                "Target": "GGShadowService", 
                "Id": "1", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Id": "10", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Id": "11", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/rejected"
            }, 
            {
                "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime", 
                "Id": "2", 
                "Subject": "/topic/state"
            }, 
            {
                "Source": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Target": "GGShadowService", 
                "Id": "3", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/RobotArm_Thing", 
                "Id": "4", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/delta"
            }, 
            {
                "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:uptimeLambda:storyLineUptime", 
                "Target": "cloud", 
                "Id": "5", 
                "Subject": "/topic/metering"
            }, 
            {
                "Source": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage", 
                "Target": "GGShadowService", 
                "Id": "6", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update"
            }, 
            {
                "Source": "cloud", 
                "Target": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:messageLambda:storyLineMessage", 
                "Id": "7", 
                "Subject": "/topic/update"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
                "Id": "8", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/accepted"
            }, 
            {
                "Source": "GGShadowService", 
                "Target": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/Switch_Thing", 
                "Id": "9", 
                "Subject": "$aws/things/RobotArm_Thing/shadow/update/rejected"
            }
        ]
    }, 
    "Version": "c75cc6e2-af57-48fb-8f6a-42e9a5f74d10", 
    "CreationTimestamp": "2017-12-29T13:14:34.243Z", 
    "Id": "2eb1f6e4-a960-4796-8e0d-e67adbdeabd7", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/subscriptions/2eb1f6e4-a960-4796-8e0d-e67adbdeabd7/versions/c75cc6e2-af57-48fb-8f6a-42e9a5f74d10"
}

ロガー定義の作成

 AWS Greengrass Core や Lambda Function でのログの出力先を指定するためのロガー定義を作成します。まずは定義を作成します。

$ aws greengrass create-logger-definition --name "RobotArm_LoggerDefinition"
{
    "Name": "RobotArm_LoggerDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T13:27:55.886Z", 
    "CreationTimestamp": "2017-12-29T13:27:55.886Z", 
    "Id": "ee0ee151-cc1e-4959-a810-0cc15d4f32fa", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/loggers/ee0ee151-cc1e-4959-a810-0cc15d4f32fa"
}

 作成されたロガー定義は下記のように参照可能です。

$ aws greengrass get-logger-definition --logger-definition-id ee0ee151-cc1e-4959-a810-0cc15d4f32fa
{
    "Name": "RobotArm_LoggerDefinition", 
    "LastUpdatedTimestamp": "2017-12-29T13:27:55.886Z", 
    "CreationTimestamp": "2017-12-29T13:27:55.886Z", 
    "Id": "ee0ee151-cc1e-4959-a810-0cc15d4f32fa", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/loggers/ee0ee151-cc1e-4959-a810-0cc15d4f32fa"
}

 続いてバージョンを作成します。 --logger-definition-id には先ほど作成した定義情報の ID を指定します。 --loggers には各ログの出力設定を json で指定します。 Component で Greengrass Core か Lambda Function かを指定します。保存先としては Greengrass Core デバイスのファイルシステムか CloudWatch を指定できます。今回はファイルシステムに保存するため、 Type には FileSystem を指定しています。 Id には任意の値を指定できますので、今回はそれぞれ system-logs と lambda-logs を指定します。

$ aws greengrass create-logger-definition-version --logger-definition-id "ee0ee151-cc1e-4959-a810-0cc15d4f32fa" --loggers '[
{
    "Id": "system-logs",
    "Component": "GreengrassSystem",
    "Level": "INFO",
    "Space": 5120,
    "Type": "FileSystem"
},
{
    "Id": "lambda-logs",
    "Component": "Lambda",
    "Level": "DEBUG",
    "Space": 5120,
    "Type": "FileSystem"
}]'
{
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/loggers/ee0ee151-cc1e-4959-a810-0cc15d4f32fa/versions/3f1abcab-6b7a-44a0-b5f6-01bc3139334c", 
    "Version": "3f1abcab-6b7a-44a0-b5f6-01bc3139334c", 
    "CreationTimestamp": "2017-12-29T13:31:19.917Z", 
    "Id": "ee0ee151-cc1e-4959-a810-0cc15d4f32fa"
}

 作成されたバージョン情報は下記のように参照できます。

$ aws greengrass get-logger-definition-version --logger-definition-id ee0ee151-cc1e-4959-a810-0cc15d4f32fa --logger-definition-version-id 3f1abcab-6b7a-44a0-b5f6-01bc3139334c
{
    "Definition": {
        "Loggers": [
            {
                "Type": "FileSystem", 
                "Space": 5120, 
                "Component": "Lambda", 
                "Id": "lambda-logs", 
                "Level": "DEBUG"
            }, 
            {
                "Type": "FileSystem", 
                "Space": 5120, 
                "Component": "GreengrassSystem", 
                "Id": "system-logs", 
                "Level": "INFO"
            }
        ]
    }, 
    "Version": "3f1abcab-6b7a-44a0-b5f6-01bc3139334c", 
    "CreationTimestamp": "2017-12-29T13:31:19.917Z", 
    "Id": "ee0ee151-cc1e-4959-a810-0cc15d4f32fa", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/loggers/ee0ee151-cc1e-4959-a810-0cc15d4f32fa/versions/3f1abcab-6b7a-44a0-b5f6-01bc3139334c"
}

AWS Greengrass Group の更新

 ここまでで作成した各コンポーネントを使用するように、最初に定義だけ作成した Greeengrass Group のバージョン情報を作成します。パラメータに Greengrass Core 定義バージョンなどの arn を指定します。

$ aws greengrass create-group-version --group-id "c4768328-4dce-4e41-9fd2-3ce8e943072e" \
                --core-definition-version-arn "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/cores/5272397b-ec69-4259-bb5c-6043daade271/versions/c72d9131-69ab-4be5-9123-2ee9709d193b" \
                --function-definition-version-arn "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/functions/1ed0c6f7-c34e-4ed1-a8d4-740f99d62778/versions/a203e6af-74b8-4a52-baf4-10c648e0f3fb" \
                --device-definition-version-arn "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/devices/081b65a9-e057-4395-80f0-d455f224eb23/versions/4a19989a-7fa7-4322-8425-f8570f625758" \
                --logger-definition-version-arn "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/loggers/ee0ee151-cc1e-4959-a810-0cc15d4f32fa/versions/3f1abcab-6b7a-44a0-b5f6-01bc3139334c" \
                --subscription-definition-version-arn "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/subscriptions/2eb1f6e4-a960-4796-8e0d-e67adbdeabd7/versions/c75cc6e2-af57-48fb-8f6a-42e9a5f74d10"
{
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/groups/c4768328-4dce-4e41-9fd2-3ce8e943072e/versions/fa439940-037b-431b-9103-3f65e8d52032", 
    "Version": "fa439940-037b-431b-9103-3f65e8d52032", 
    "CreationTimestamp": "2017-12-29T13:45:10.072Z", 
    "Id": "c4768328-4dce-4e41-9fd2-3ce8e943072e"
}

 作成されたバージョン情報は下記のように参照できます。

$ aws greengrass get-group-version --group-id c4768328-4dce-4e41-9fd2-3ce8e943072e --group-version-id fa439940-037b-431b-9103-3f65e8d52032
{
    "Definition": {
        "CoreDefinitionVersionArn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/cores/5272397b-ec69-4259-bb5c-6043daade271/versions/c72d9131-69ab-4be5-9123-2ee9709d193b", 
        "LoggerDefinitionVersionArn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/loggers/ee0ee151-cc1e-4959-a810-0cc15d4f32fa/versions/3f1abcab-6b7a-44a0-b5f6-01bc3139334c", 
        "FunctionDefinitionVersionArn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/functions/1ed0c6f7-c34e-4ed1-a8d4-740f99d62778/versions/a203e6af-74b8-4a52-baf4-10c648e0f3fb", 
        "DeviceDefinitionVersionArn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/devices/081b65a9-e057-4395-80f0-d455f224eb23/versions/4a19989a-7fa7-4322-8425-f8570f625758", 
        "SubscriptionDefinitionVersionArn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/definition/subscriptions/2eb1f6e4-a960-4796-8e0d-e67adbdeabd7/versions/c75cc6e2-af57-48fb-8f6a-42e9a5f74d10"
    }, 
    "Version": "fa439940-037b-431b-9103-3f65e8d52032", 
    "CreationTimestamp": "2017-12-29T13:45:10.072Z", 
    "Id": "c4768328-4dce-4e41-9fd2-3ce8e943072e", 
    "Arn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/groups/c4768328-4dce-4e41-9fd2-3ce8e943072e/versions/fa439940-037b-431b-9103-3f65e8d52032"
}

デプロイの作成

 ここまででひとまず Greengrass Group の設定が完了したので、 Greengrass Core デバイスに設定内容をデプロイします。 --deployment-type にはデプロイ種別を指定します。今回は新規のデプロイなので、 NewDeployment を指定します。

$ aws greengrass create-deployment --deployment-type NewDeployment --group-id c4768328-4dce-4e41-9fd2-3ce8e943072e --group-version-id fa439940-037b-431b-9103-3f65e8d52032
{
    "DeploymentId": "4b5a6bb0-bc30-40f7-8911-7d056f35b702", 
    "DeploymentArn": "arn:aws:greengrass:ap-northeast-1:XXXXXXXXXXXX:/greengrass/groups/c4768328-4dce-4e41-9fd2-3ce8e943072e/deployments/4b5a6bb0-bc30-40f7-8911-7d056f35b702"
}

 デプロイのステータスは下記のように参照可能です。

$ aws greengrass get-deployment-status --deployment-id 4b5a6bb0-bc30-40f7-8911-7d056f35b702 --group-id c4768328-4dce-4e41-9fd2-3ce8e943072e
{
    "DeploymentType": "NewDeployment", 
    "DeploymentStatus": "InProgress", 
    "UpdatedAt": "2017-12-29T13:51:52.407Z"
}

 まだ Greengrass Core デバイスを起動しておらず、デプロイが受け取られていないため、 DeploymentStatus が InProgress になっています。デバイスが起動してデプロイが受け取られると Success に変わります。

Greengrass Core 接続情報の更新

 他のデバイスが Greengrass Core に接続するための情報を更新します。 Core と連携デバイスはまずクラウドにアクセスしてこの情報を参照した上で、対象の Core デバイスに接続することになります。 --thing-name には Greengrass Core デバイスの Thing 名を指定します。 --connectivity-info には接続情報のリストを json で渡します。 Id には任意の値を指定できるのでここでは 1 としています。 HostAdress は Core デバイスの IP アドレスを指定し、 PortNumber には Core デバイスのポート番号を指定します。

$ aws greengrass update-connectivity-info --thing-name "GGC_Thing" --connectivity-info '[
{
    "Id": "1",
    "HostAddress": "192.168.10.11",
    "PortNumber": 8883
}]'
{
    "Version": "e112af49-c242-4899-8d6d-c4e8d5017cf6"
}

 更新された接続情報は下記のように参照できます。

$ aws greengrass get-connectivity-info --thing-name GGC_Thing
{
    "ConnectivityInfo": [
        {
            "PortNumber": 8883, 
            "HostAddress": "192.168.10.11", 
            "Id": "1"
        }
    ]
}

デバイス上で動かすプログラムの用意

 Switch_Thing と RobotArm_Thing で動作させるためのプログラムを用意します。今回はお試しということで、ローカルの Mac 上に Vagrant で Ubuntu の VM を作成してそこで実行します。(Vagrant での環境の作り方等は割愛) プログラムはこのシナリオのサンプル用としてあらかじめ提供されているものをそのまま使用します。

 まずは AWS IoT デバイス SDK for C++ のリポジトリを git clone します。

$ git clone https://github.com/aws/aws-iot-device-sdk-cpp.git
Cloning into 'aws-iot-device-sdk-cpp'...
remote: Counting objects: 554, done.
remote: Total 554 (delta 0), reused 0 (delta 0), pack-reused 554
Receiving objects: 100% (554/554), 547.85 KiB | 525.00 KiB/s, done.
Resolving deltas: 100% (192/192), done.
Checking connectivity... done.

 リポジトリのディレクトリに入って build 用のディレクトリを作成します。

$ cd aws-iot-device-sdk-cpp/
$ mkdir build

 Makefile を作成します。

$ cd build
$ cmake ../.

 RobotArm_Thing 用のプログラムをビルドします。

$ make robot-arm-sample

 Swith_Thing 用のプログラムをビルドします。

$ make switch-sample

 ビルドが成功していれば、 build/bin ディレクトリにバイナリが生成されています。

$ ls -ltr bin/
total 7712
-rwxr-xr-x 1 vagrant vagrant 4148216 Dec 29 14:19 robot-arm-sample
drwxr-xr-x 1 vagrant vagrant     136 Dec 29 14:20 certs
-rwxr-xr-x 1 vagrant vagrant 3747600 Dec 29 14:20 switch-sample
drwxr-xr-x 1 vagrant vagrant     136 Dec 29 14:20 config

 RobotArm_Thing と Swith_Thing それぞれの証明書とプライベートキー、AWS IoT CA 証明書を build/bin/certs ディレクトリ配下の robotArm ディレクトリと switch ディレクトリに格納します。

$ ls -ltr bin/certs/robotArm/
total 12
-rw-r--r-- 1 vagrant vagrant 1758 Dec 29 14:35 root-ca.pem
-rw-r--r-- 1 vagrant vagrant 1679 Dec 29 14:35 c5e6d39f7b-private.pem.key
-rw-r--r-- 1 vagrant vagrant 1224 Dec 29 14:35 c5e6d39f7b-certificate.pem.crt
$
$ ls -ltr bin/certs/switch/
total 12
-rw-r--r-- 1 vagrant vagrant 1758 Dec 29 14:35 root-ca.pem
-rw-r--r-- 1 vagrant vagrant 1675 Dec 29 14:35 a20b621e05-private.pem.key
-rw-r--r-- 1 vagrant vagrant 1220 Dec 29 14:35 a20b621e05-certificate.pem.crt

 各プログラムの設定ファイルは build/bin/config ディレクトリにあります。

$ ls -ltr bin/config/
total 8
-rw-r--r-- 1 vagrant vagrant 836 Dec 29 14:19 RobotArmConfig.json
-rw-r--r-- 1 vagrant vagrant 875 Dec 29 14:20 SwitchConfig.json

 それぞれ AWS IoT エンドポイントと証明書などの設定を更新します。更新後の差分は下記の通り。

$ diff RobotArmConfig.json.bak RobotArmConfig.json
2c2
<   "endpoint": "",
---
>   "endpoint": "greengrass.iot.ap-northeast-1.amazonaws.com",
6,8c6,8
<   "root_ca_relative_path": "",
<   "device_certificate_relative_path": "",
<   "device_private_key_relative_path": "",
---
>   "root_ca_relative_path": "certs/robotArm/root-ca.pem",
>   "device_certificate_relative_path": "certs/robotArm/c5e6d39f7b-certificate.pem.crt",
>   "device_private_key_relative_path": "certs/robotArm/c5e6d39f7b-private.pem.key",
$
$ diff SwitchConfig.json.bak SwitchConfig.json
2c2
<   "endpoint": "",
---
>   "endpoint": "greengrass.iot.ap-northeast-1.amazonaws.com",
6,8c6,8
<   "root_ca_relative_path": "",
<   "device_certificate_relative_path": "",
<   "device_private_key_relative_path": "",
---
>   "root_ca_relative_path": "certs/robotArm/root-ca.pem",
>   "device_certificate_relative_path": "certs/switch/a20b621e05-certificate.pem.crt",
>   "device_private_key_relative_path": "certs/switch/a20b621e05-private.pem.key",

Greengrass Core の実行

 ここまでで RobotArm_Thing と Swith_Thing でのプログラムの実行準備はできたので、先に Greengrass Core を起動します。 Greengrass Core の設定や起動等については前回の記事を参照ください。今回も同様に Raspberry Pi で稼働させます。

$ sudo ./greengrassd start
Stopping greengrass daemon of PID: 1260
Waiting.
Stopped greengrass daemon, exiting with success
Setting up greengrass daemon
Validating hardlink/softlink protection
Validating execution environment
Found cgroup subsystem: cpu
Found cgroup subsystem: cpuacct
Found cgroup subsystem: blkio
Found cgroup subsystem: memory
Found cgroup subsystem: devices
Found cgroup subsystem: freezer
Found cgroup subsystem: net_cls

Starting greengrass daemon
Greengrass successfully started with PID: 5588

 ちなみに Greengrass Core を起動すると先ほどのデプロイが実行されるので、ステータスが Success に変わります。

$ aws greengrass get-deployment-status --deployment-id 4b5a6bb0-bc30-40f7-8911-7d056f35b702 --group-id c4768328-4dce-4e41-9fd2-3ce8e943072e
{
    "DeploymentType": "NewDeployment", 
    "DeploymentStatus": "Success", 
    "UpdatedAt": "2017-12-29T15:00:14.785Z"
}

デバイスコードの実行

 それでは RobotArm_Thing と Switch_Thing でのプログラムを実行します。まずは RobotArm_Thing のプログラムです。 build/bin ディレクトリ配下の robot-arm-sample を実行します。

$ ./robot-arm-sample 
[INFO] Fri Dec 29 15:07:49 2017
:878 [OpenSSL Wrapper] [140324204885824] [ConnectTCPSocket:L215] : resolved greengrass.iot.ap-northeast-1.amazonaws.com to 52.192.148.221
[INFO] Fri Dec 29 15:07:52 2017
:337 [Sample - RobotArm] [140324204885824] [RunSample:L168] : GGC connectivity information found for this Device! 400

[INFO] Fri Dec 29 15:07:52 2017
:346 [Sample - RobotArm] [140324204885824] [RunSample:L222] : Attempting Connect with:
GGC Endpoint : 192.168.10.11
GGC Endpoint Port : 8883

[INFO] Fri Dec 29 15:07:52 2017
:347 [Sample - RobotArm] [140324204885824] [RunSample:L230] : Using CA at : /vagrant/aws-iot-device-sdk-cpp/build/bin/c4768328-4dce-4e41-9fd2-3ce8e943072e_root_ca1.pem

[INFO] Fri Dec 29 15:07:52 2017
:351 [OpenSSL Wrapper] [140324204885824] [ConnectTCPSocket:L215] : resolved 192.168.10.11 to 192.168.10.11
[INFO] Fri Dec 29 15:07:52 2017
:538 [Network Read] [140324161402624] [HandleConnack:L228] : Network Connect Response. Success : SDK Code 0.
[INFO] Fri Dec 29 15:07:53 2017
:39 [Sample - RobotArm] [140324204885824] [RunSample:L248] : Connected to GGC arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/GGC_Thing in Group c4768328-4dce-4e41-9fd2-3ce8e943072e!!

Sending Inital State ------- 
{"state":{"reported":{"myState":"off"}}}

Waiting for an update!

 このプログラムは起動時に状態を off として Shadow を更新して待機します。起動後に AWS IoT コンソールから RobotArm_Thing の Device Shadow を確認すると下記のようになっています。

{
  "reported": {
    "myState": "off"
  }
}

 myState に変更があると /topic/metering トピックにメッセージを発行します。

 それでは続いて Switch_Thing のプログラムを起動します。 build/bin ディレクトリ配下の switch-sample を実行します。

$ ./switch-sample 
[INFO] Fri Dec 29 15:13:21 2017
:47 [OpenSSL Wrapper] [140588581013312] [ConnectTCPSocket:L215] : resolved greengrass.iot.ap-northeast-1.amazonaws.com to 52.192.176.176
[INFO] Fri Dec 29 15:13:25 2017
:358 [Sample - Switch] [140588581013312] [RunSample:L139] : GGC connectivity information found for this Device!!

[INFO] Fri Dec 29 15:13:25 2017
:363 [Sample - Switch] [140588581013312] [RunSample:L193] : Attempting Connect with:
GGC Endpoint : 192.168.10.11
GGC Endpoint Port : 8883

[INFO] Fri Dec 29 15:13:25 2017
:364 [Sample - Switch] [140588581013312] [RunSample:L201] : Using CA at : /vagrant/aws-iot-device-sdk-cpp/build/bin/c4768328-4dce-4e41-9fd2-3ce8e943072e_root_ca1.pem

[INFO] Fri Dec 29 15:13:25 2017
:369 [OpenSSL Wrapper] [140588581013312] [ConnectTCPSocket:L215] : resolved 192.168.10.11 to 192.168.10.11
[INFO] Fri Dec 29 15:13:25 2017
:562 [Network Read] [140588537530112] [HandleConnack:L228] : Network Connect Response. Success : SDK Code 0.
[INFO] Fri Dec 29 15:13:26 2017
:63 [Sample - Switch] [140588581013312] [RunSample:L219] : Connected to GGC arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:thing/GGC_Thing in Group c4768328-4dce-4e41-9fd2-3ce8e943072e!!

Please enter 1 (turn on) or 0 (turn off) to control the robot arm, q to quit: 

 入力待ち状態になりますので、 RobotArm_Thing の状態を on にする場合は 1 を、 off にする場合は 0 を入力します。終了するには q を入力します。先ほどの起動時は off になっていますので、 on にしてみます。

Please enter 1 (turn on) or 0 (turn off) to control the robot arm, q to quit: 1

Publishing message to cloud
{"state":{"desired":{"myState":"on"}}}

 RobotArm_Thing のプログラムのコンソールには下記のように出力されます。

-- Published state to /topic/metering (Should be routed to uptimelambda!) --
------- Robot Arm State --------
on

 Device Shadow を確認すると下記のように更新されています。

{
  "desired": {
    "myState": "on"
  },
  "reported": {
    "myState": "on"
  }
}

 また、 /topic/metering トピックには下記のようなメッセージが publish されています。

Robot arm turned ON

 今度はクラウド側から状態を変更してみます。 /topic/update に Subscribe し、下記の json を publish します。

{"state":"off"}

 すると RobotArm_Thing のプログラムを動かしているコンソールで下記のように出力されます。

-- Published state to /topic/metering (Should be routed to uptimelambda!) --
------- Robot Arm State --------
off

 Device Shadow は下記のようになります。

{
  "desired": {
    "myState": "off"
  },
  "reported": {
    "myState": "off"
  }
}

 /topic/metering には下記のように publish されます。

Robot arm turned OFF

まとめ

 ひとまず Greengrass Core と他のデバイスを連携させて動かすことができましたが、今回は用意されているシナリオをそのまま動かしただけで、具体的にデバイスで動かすプログラムはどのように実装したら良いのか、サブスクリプションの各設定はどのような意味なのかはまだちゃんと理解できていません。コンソールから設定を変更してデプロイすることで各デバイスに反映されるのは便利だと思いますので、使い方をもう少し把握したいと思います。また、今回のシナリオで用意されていたデバイスコードは C++ のコードでしたが、 Python 等でも実装できそうなので、そちらも確認してみたいと思います。

AWS Greengrass を Raspberry Pi で動かしてみる

 AWS re:Invent 2017 では今年も多数の新サービスが発表されましたね。その中には IoT や AI 関連のものも多く、 エッジデバイス上で Machine Learning の推論が実行できる AWS Greengrass ML Inference などはとても興味深いです。が、そもそも Greengrass に今まで触ったことがなかったので、今更ではありますが Raspberry Pi で Greengrass を動かしてみました。基本的に下記の公式ドキュメントのチュートリアルの実行です。

docs.aws.amazon.com

Raspberry Pi での環境設定

 Raspberry Pi の基本的な環境構築は終わっているものとして、 Greengrass を動かすための設定を行います。まずは Greengrass Core 用の Linux ユーザとグループを作成し、 sqlite3 をインストールします。

pi@raspberrypi:~ $ sudo adduser --system ggc_user
Adding system user `ggc_user' (UID 117) ...
Adding new user `ggc_user' (UID 117) with group `nogroup' ...
Creating home directory `/home/ggc_user' ...
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo addgroup --system ggc_group
Adding group `ggc_group' (GID 122) ...
Done.
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo apt-get install sqlite3
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Suggested packages:
  sqlite3-doc
The following NEW packages will be installed:
  sqlite3
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 99.6 kB of archives.
After this operation, 139 kB of additional disk space will be used.
Get:1 http://mirrordirector.raspbian.org/raspbian/ jessie/main sqlite3 armhf 3.8.7.1-1+deb8u2 [99.6 kB]
Fetched 99.6 kB in 1s (91.6 kB/s)
Selecting previously unselected package sqlite3.
(Reading database ... 104790 files and directories currently installed.)
Preparing to unpack .../sqlite3_3.8.7.1-1+deb8u2_armhf.deb ...
Unpacking sqlite3 (3.8.7.1-1+deb8u2) ...
Processing triggers for man-db (2.7.5-1~bpo8+1) ...
Setting up sqlite3 (3.8.7.1-1+deb8u2) ...
pi@raspberrypi:~ $ 

 Greengrass Core では起動時にOSでハードリンク/ソフトリンクの保護が有効か確認しているため、この保護を有効にしておきます。 /etc/sysctl.d/98-rpi.conf に設定を追加します。追加前後の差分は下記の通りです。

pi@raspberrypi:~ $ diff /etc/sysctl.d/98-rpi.conf.20171220 /etc/sysctl.d/98-rpi.conf
2a3,4
> fs.protected_hardlinks = 1
> fs.protected_symlinks = 1
pi@raspberrypi:~ $ 

 設定を追加したら一度再起動します。

pi@raspberrypi:~ $ sudo reboot

 再起動したら下記のように確認するとハードリンク/ソフトリンクの保護が有効になっていることがわかります。

pi@raspberrypi:~ $ sudo sysctl -a | grep 'fs.protected'
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
sysctl: reading key "net.ipv6.conf.all.stable_secret"
sysctl: reading key "net.ipv6.conf.default.stable_secret"
sysctl: reading key "net.ipv6.conf.eth0.stable_secret"
sysctl: reading key "net.ipv6.conf.lo.stable_secret"
sysctl: reading key "net.ipv6.conf.wlan0.stable_secret"

Greengrass Group と Greengrass Core の作成

 Greengrass のデバイスには Core デバイスと、 Core デバイスに接続するそれ以外のデバイスがあり、Core デバイスが AWS IoT や Greengrass のクラウドサービスと通信します。また、それらのデバイスと設定情報をひとまとまりにしたものが Greengrass Group になります。まずはコンソールから Greengrass Group を作成しますが、その前にコンソールから Greengrass を操作するための権限を追加しておきます。今回はお試しということでとりあえず Greengrass へのフルアクセス権限を追加してしまいます。

f:id:akanuma-hiroaki:20171221081929p:plain

 それでは Greengrass Group を作成します。AWS IoT のコンソールのメニューから Greengrass を選択します。

f:id:akanuma-hiroaki:20171221082356p:plain

 ようこそ画面が表示されるので、「Greengrass グループの定義」の 今すぐ始める をクリックします。

f:id:akanuma-hiroaki:20171221082445p:plain

 グループの作成方法の選択画面が表示されますので、 簡単な作成の使用 を選択します。この方法だと Core デバイスがクラウドサービスにアクセスするために必要な証明書やキーペアの作成なども自動で行ってくれます。

f:id:akanuma-hiroaki:20171221082740p:plain

 グループ名の入力画面で任意のグループ名を設定します。

f:id:akanuma-hiroaki:20171221083155p:plain

 続けて Core デバイスの名前も決めます。デフォルトだとグループ名に _Core がついたものになっているので今回はそのまま使用します。

f:id:akanuma-hiroaki:20171221083252p:plain

 実行する内容が表示されるので、 グループとコアの作成 をクリックします。

f:id:akanuma-hiroaki:20171221083411p:plain

 Group と Core が作成され、証明書のダウンロード画面が表示されますので、ダウンロードしておきます。

f:id:akanuma-hiroaki:20171221083632p:plain

 また、同じ画面で Greengrass ソフトウェアパッケージもダウンロードできますので、CPUアーキテクチャとして ARMv7l を選択し、ダウンロードして、 完了 をクリックします。

f:id:akanuma-hiroaki:20171221083804p:plain

 これで Greengrass Group と Greengrass Core の作成は完了です。

Greengrass Core のインストール

 Raspberry Pi に先ほどダウンロードした Greengrass ソフトウェアパッケージをインストールします。ソフトウェアパッケージを Raspberry Pi 上に配置したら展開します。

pi@raspberrypi:~ $ sudo tar zxf greengrass-linux-armv7l-1.3.0.tar.gz -C /
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ ls -l /greengrass/
total 16
drwxr-xr-x 2 nobody 99 4096 Nov 21 08:09 certs
drwxr-xr-x 2 nobody 99 4096 Nov 21 08:09 config
drwxr-xr-x 5 nobody 99 4096 Nov 21 08:09 ggc
drwxr-xr-x 3 nobody 99 4096 Nov 21 08:09 ota

 Lambda の cgroup を自動的に設定するために、 /etc/fstab に下記設定を追加します。

cgroup /sys/fs/cgroup cgroup defaults 0 0

 追加後の fstab は下記のようになります。設定を追加したら一度再起動しておきます。

pi@raspberrypi:~ $ tail /etc/fstab 
proc            /proc           proc    defaults          0       0
/dev/mmcblk0p1  /boot           vfat    defaults          0       2
/dev/mmcblk0p2  /               ext4    defaults,noatime  0       1
# a swapfile is not a swap partition, no line here
#   use  dphys-swapfile swap[on|off]  for that
cgroup /sys/fs/cgroup cgroup defaults 0 0

 続いて証明書を配置します。先ほど展開したディレクトリ内の certs ディレクトリに、Versign からルートCA証明書をダウンロードして配置します。

pi@raspberrypi:/greengrass/certs $ pwd
/greengrass/certs
pi@raspberrypi:/greengrass/certs $ 
pi@raspberrypi:/greengrass/certs $ sudo wget http://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem -O root-ca-cert.pem                                             
--2017-12-20 15:30:39--  http://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem
Resolving www.symantec.com (www.symantec.com)... 72.247.61.29
Connecting to www.symantec.com (www.symantec.com)|72.247.61.29|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1758 (1.7K) [text/plain]
Saving to: ‘root-ca-cert.pem’

root-ca-cert.pem                                            100%[===========================================================================================================================================>]   1.72K  --.-KB/s   in 0s     

2017-12-20 15:30:39 (37.7 MB/s) - ‘root-ca-cert.pem’ saved [1758/1758]

 続けて、 Group と Core 作成時にダウンロードした証明書の圧縮ファイルに含まれる証明書とプライベートキーを同じディレクトリに配置します。

pi@raspberrypi:~ $ tar zxf dbb10e0817-setup.tar.gz 
pi@raspberrypi:~ $ ls certs/
dbb10e0817.cert.pem  dbb10e0817.private.key  dbb10e0817.public.key
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo cp certs/dbb10e0817.private.key /greengrass/certs/.                                                                                                                                                                   
pi@raspberrypi:~ $ sudo cp certs/dbb10e0817.cert.pem /greengrass/certs/.

 次に、コンソールから Greengrass 用のサービスロールを作成します。 IAM のコンソールから ロールの作成 をクリックします。

f:id:akanuma-hiroaki:20171221085246p:plain

 サービスのリストから Greengrass を選択して 次のステップ をクリックします。

f:id:akanuma-hiroaki:20171221085325p:plain

 ポリシーの選択画面で AWSGreengrassResourceAccessRolePolicy を選択して 次のステップ をクリックします。

f:id:akanuma-hiroaki:20171221085446p:plain

 任意のロール名を入力して ロールの作成 をクリックします。

f:id:akanuma-hiroaki:20171221085631p:plain

 ロールが作成されたら ARN を記録しておきます。

f:id:akanuma-hiroaki:20171221085839p:plain

 続いて今作成したサービスロールをアカウントに紐づけます。 Mac 上から AWS CLI で下記のようにコマンドを実行します。

Greengrass  $ aws greengrass associate-service-role-to-account --role-arn arn:aws:iam::365361468908:role/Greengrass
2017-12-20T15:53:26Z

 最後に Raspberry Pi 上の Greengrass 設定ファイルを下記のように編集します。各証明書などへのパスはフルパスではなくファイル名だけでOKです。

pi@raspberrypi:/greengrass/ggc/packages/1.3.0 $ cat /greengrass/config/config.json 
{
    "coreThing": {
        "caPath": "root-ca-cert.pem",
        "certPath": "dbb10e0817.cert.pem",
        "keyPath": "dbb10e0817.private.key",
        "thingArn": "arn:aws:iot:ap-northeast-1:365361468908:thing/MyGroup_Core",
        "iotHost": "xxxxxxxxxxxxxx.iot.ap-northeast-1.amazonaws.com",
        "ggHost": "greengrass.iot.ap-northeast-1.amazonaws.com"
    },
    "runtime": {
        "cgroup": {
            "useSystemd": "yes"
        }
    },
    "managedRespawn": false
}

Greengrass Core の起動

 それでは Raspberry Pi 上の Greengrass Core を起動します。 Greengrass のソフトウェアパッケージのディレクトリ(今回は /greengrass/ggc/packages/1.3.0 )で下記のようにコマンドを実行します。

pi@raspberrypi:/greengrass/ggc/packages/1.3.0 $ sudo ./greengrassd start
Setting up greengrass daemon
Validating hardlink/softlink protection
Validating execution environment
Found cgroup subsystem: cpu
Found cgroup subsystem: cpuacct
Found cgroup subsystem: blkio
Found cgroup subsystem: memory
Found cgroup subsystem: devices
Found cgroup subsystem: freezer
Found cgroup subsystem: net_cls

Starting greengrass daemon
Greengrass successfully started with PID: 1917

 成功するとデーモンが起動します。

pi@raspberrypi:/greengrass/ggc/packages/1.3.0 $ ps aux | grep greengrass | grep -v grep
root      1304  0.7  1.4 940632 13900 pts/0    Sl   Dec20   0:38 /greengrass/ggc/packages/1.3.0/bin/daemon -core-dir=/greengrass/ggc/packages/1.3.0 -port=8000 -connectionManager=true -router=true -shadow=true -shadowSync=true -tes=true -deviceCertificateManager=true
ggc_user  1472  0.8  1.1  31308 10796 ?        Ssl  Dec20   0:45 python2.7 /runtime/python2.7/lambda_runtime.py --handler=greengrassHelloWorld.function_handler

Lambda Function の作成

 Greengrass Core デバイスで動作させるための Lambda Function を作成します。 Lambda のコンソールから 関数の作成 をクリックします。

f:id:akanuma-hiroaki:20171221090835p:plain

 今回はあらかじめ用意されているサンプルを使用しますので、「設計図」から「greengrass-hello-world」を選択して 設定 をクリックします。

f:id:akanuma-hiroaki:20171221090927p:plain

 任意の Function 名とロールを選択します。今回は以前作成していたロールを使用していますが、作成していなかった場合は新たに作成します。

f:id:akanuma-hiroaki:20171221091049p:plain

 Core デバイスで Lambda Function を実行するには新しいバージョンを発行しておく必要があるので、「アクション」メニューから 新しいバージョンを発行 をクリックします。

f:id:akanuma-hiroaki:20171221091156p:plain

 バージョンの説明を入力して 発行 をクリックします。

f:id:akanuma-hiroaki:20171221091339p:plain

Greengrass Group に Lambda Function を追加

 作成した Lambda Function を Greengrass Group に追加します。 Greengrass Group のコンソールの Lambda メニューから、 最初の Lambda を追加する をクリックします。

f:id:akanuma-hiroaki:20171221091712p:plain

 Lambda の追加方法の選択画面が表示されますので、 既存の Lambda の使用 を選択します。

f:id:akanuma-hiroaki:20171221092014p:plain

 Lambda Function の選択画面が表示されますので、先ほど作成した Lambda Function を選択して 次へ をクリックします。

f:id:akanuma-hiroaki:20171221092126p:plain

 先ほど発行したバージョンを選択して 完了 をクリックします。

f:id:akanuma-hiroaki:20171221092229p:plain

 Lambda Function が追加されたら、設定を変更するために 設定の編集 をクリックします。

f:id:akanuma-hiroaki:20171221092414p:plain

 設定項目の中の「Lambda のライフサイクル」の項目で「存続期間が長く無制限に稼働する関数にする」を選択して 更新 をクリックします。

f:id:akanuma-hiroaki:20171221092505p:plain

Greengrass Group にサブスクリプションを追加

 Greengrass Core が MQTT プロトコルでメッセージをやりとりするためのサブスクリプションを追加します。サブスクリプションは、メッセージの送信元であるソース、メッセージの送信先であるターゲット、それとトピックから構成されます。 Greengrass のサブスクリプションメニューから、 最初のサブスクリプションを追加 をクリックします。

f:id:akanuma-hiroaki:20171221232433p:plain

 ソースの選択で先ほど作成した Lambda Function を選択し、ターゲットの選択では IoT Cloud を選択して 次へ をクリックします。

f:id:akanuma-hiroaki:20171221232652p:plain

 今回は対象のトピックを hello/world に限定するため、「オプションのトピックフィルター」に hello/world を入力して 次へ をクリックします。

f:id:akanuma-hiroaki:20171221232849p:plain

 最後に 完了 をクリックしてサブスクリプションの追加は完了です。

f:id:akanuma-hiroaki:20171221233139p:plain

Greengrass Group のデプロイ

 ここまででクラウド上での Greengrass Group の設定は完了したので、これを Greengrass Core デバイスにコピーします。 Greengrass Group のデプロイのコンソールのアクションから デプロイ を選択します。

f:id:akanuma-hiroaki:20171221233546p:plain

 検出方法の設定画面になりますので、 自動検出 を選択します。

f:id:akanuma-hiroaki:20171221233654p:plain

 これまでの設定が正しく行われ、 Core デバイス上でデーモンが正しく稼働していれば、少し経つとデプロイが完了します。

f:id:akanuma-hiroaki:20171221233809p:plain

動作確認

 それでは Greengrass Core デバイス上で Lambda Function が正しく実行されているか確認します。AWS IoT コンソールから「テスト」を選択し、 hello/world トピックにサブスクライブします。

f:id:akanuma-hiroaki:20171221234017p:plain

 Lambda Function が正しく実行されていれば、下記のようにトピックにメッセージが発行されていきます。

f:id:akanuma-hiroaki:20171221234034p:plain

まとめ

 今回はチュートリアルの内容をそのまま追っただけになってしまいましたが、 Greengrass Core デバイス(今回は Raspberry Pi)上で Lambda Function を動かせることが確認できました。今回はここまでで長くなってしまいましたが、まだ Core デバイスに接続している他のデバイスとの連携は試せていないので、次回以降で試してみたいと思います。