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 デバイスに接続している他のデバイスとの連携は試せていないので、次回以降で試してみたいと思います。

Arm Mbed Cloud によるデバイス管理&リモートアップデート

 12/8(金)に Arm Mbed Connect 2017 のワークショップに参加してきました。

armkk-event.com

 このワークショップでは Mbed 対応の開発ボードで実際にコーディングしたり、 Mbed Cloud でのデバイス管理などを体験できるハンズオン型のワークショップで、下記のような内容でした。

  1. Mbed ビルドツールのインストール
  2. Mbed OS - 自分のアプリケーションをビルドする
  3. Mbed Cloud Client - Mbed Cloud アプリケーションをビルドする
  4. Mbed Cloud Client - 新しい LWM2M オブジェクトを追加する
  5. Mbed Cloud Client - 書き込み可能な LWM2M オブジェクトを追加する
  6. Mbed Cloud Client - Wi-Fi アップデート
  7. Cloud SDK – 値を取得するシンプルなwebアプリ
  8. デバッガの使用

 Mbed OS アプリのビルドは Mbed CLI を使ったものでしたが、それについては以前も書いていますので、今回は Mbed Cloud でのデバイス管理の部分について、どのような感じで管理・アップデートが行えるのかを紹介してみたいと思います。

使用デバイス

 今回使用したデバイスはワークショップ用に用意された開発ボードで、ワークショップ後はそのままいただくことができました。無料ワークショップなのになんとも太っ腹です。

f:id:akanuma-hiroaki:20171212081717j:plain:w450 f:id:akanuma-hiroaki:20171212081735j:plain:w450

 機能としては下記のようなものが載っています。

  • Wi-Fi

  • センサー(温度、湿度、照度、加速度、大気クオリティ、距離)

  • RGB LED とステータス表示用の LED

  • LCD ディスプレイ

  • スイッチ

 ワークショップ中に使ったセンサーは温度センサーだけでしたが、それ以外にも色々なセンサーが載っているので遊べそうです。OS は Mbed OS 5 です。

Mbed Cloud とは

 Mbed Cloud は IoT デバイスを管理するためのクラウドプラットフォームで、Mbed OS 5 と連携することで個別の IoT デバイスを管理することができます。

cloud.mbed.com

 現在 Mbed Cloud はパートナーしか利用することができませんが、今回のワークショップでは参加者に期間限定のアカウントが提供され、それを使ってアクセスしました。

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

 ログインするとダッシュボードが表示されます。

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

Mbed Cloud へのデバイス登録

 Mbed Cloud にデバイスを接続するには Mbed Cloud で生成した証明書を使用する必要があります。Mbed Cloud の Device identity メニューの Actions から Create a developer certificate を選択し、ダイアログに従って情報を登録して行くと証明書が作成されます。

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

 作成された証明書を一覧から選択してローカルにダウンロードし、アプリケーションのルートディレクトリに配置します。

 Mbed Cloud では LWM2M を使ってデバイスの管理が行われますので、デバイスの登録も LWM2M のクライアントを使います。今回のワークショップでは登録用のサンプルコードが提供されていたので、自分で書く必要はありませんでしたが、アプリ側の登録用コードの抜粋は下記のようになります。

client = new SimpleM2MClient(network, &sd, &fs);
int init_rt = client->init();
client->call_register();

 そして下記のようにビルドして、生成されたバイナリ combined.bin をボードにコピーして書き込みます。

$ mbed compile -t GCC_ARM -m UBLOX_EVK_ODIN_W2
〜〜〜中略〜〜〜
Combined binary: /Users/akanuma/Documents/mbed_connect_ws/mac-workshop-content/mbed-connect-cloud-application/BUILD/UBLOX_EVK_ODIN_W2/GCC_ARM/combined.bin
〜〜〜中略〜〜〜
Image: ./BUILD/UBLOX_EVK_ODIN_W2/GCC_ARM/mbed-connect-cloud-application.bin

 正しく登録されると、 Mbed Cloud の Device directory に対象のデバイスが表示されます。

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

デバイス情報の確認

 登録されたデバイスの情報を確認するには、デバイスの一覧から対象のデバイスの Device ID をクリックします。表示された詳細画面から Live resources タブをクリックすると、 LWM2M の各リソースの情報が確認できます。表示されるリソースの項目や内容は各デバイスでの実装に依存します。

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

 また、更新が可能なリソースはこの画面から値を更新することも可能です。各リソースが更新可能かどうかはデバイス側での実装に依存します。

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

Wi-Fi アップデートの準備

 IoT デバイスでは Web アプリケーションやスマートフォンアプリケーションと違って、デバイス自体を遠隔地において来てしまうので簡単にファームウェアのアップデートのフラッシュを行うことができません。そこで IoT プラットフォームによる、デバイスのファームウェアのリモートアップデート機能が重要になってきます。 Mbed Cloud でもこの機能を持っており、 Wi-Fi に接続した上でソフトウェアをリモートでアップデートできます。

 Mbed Cloud のリモートアップデートでは manifest-tool を使いますが、そのために API キーが必要になりますので、Mbed Cloud のコンソールから API キーを生成します。 Access management メニューの API keys の画面から Create new API key をクリックして表示されるダイアログに従うと API キーが生成されて表示されます。API キーの内容は生成時しか見ることはできないため、ここでコピーして保存しておきます。

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

 次に manifest-tool がインストールされていない場合はインストールします。ワークショップ時はインストール用のファイルが配布されたので pip でローカルファイルを使ってインストールしましたが、通常はこちらのチュートリアルの手順に従ってインストールすることになるかと思います。

https://cloud.mbed.com/docs/v1.2/updating-firmware/tutorial-installing-the-manifest-tool.html

 ただ、このチュートリアルの手順だと manifest-tool のリポジトリが見つからないということでエラーになってしまうため、実際に使う場合は確認が必要です。

$ pip install -U "git+https://github.com/ARMmbed/manifest-tool-restricted.git"
Collecting git+https://github.com/ARMmbed/manifest-tool-restricted.git
  Cloning https://github.com/ARMmbed/manifest-tool-restricted.git to /private/var/folders/l1/5gdn8snd6gj_nfyh5j4nc1sw0000gn/T/pip-rx_uWM-build
remote: Repository not found.
fatal: repository 'https://github.com/ARMmbed/manifest-tool-restricted.git/' not found
Command "git clone -q https://github.com/ARMmbed/manifest-tool-restricted.git /private/var/folders/l1/5gdn8snd6gj_nfyh5j4nc1sw0000gn/T/pip-rx_uWM-build" failed with error code 128 in None

 ここではひとまず manifest-tool がインストールできたことにして、次にアプリケーションのルートディレクトリで manifest-tool を設定します。コマンドのシグネチャは $ manifest-tool init -d "<company domain name>" -m "<product model ID>" -a "<api key>" -q --force という感じなので例えば、

$ manifest-tool init -d "example.com" -m "update_test" -a "ak_1MDE2MDBmYWM0YjBjXXXXXXXXXXXXXXXXXXXX" -q --force

 という感じになります。これで公開鍵/秘密鍵のペアが作成されて公開鍵はデバイスに組み込まれるようになりますので、秘密鍵はプログラムの更新時に署名するために保持しておきます。 manifest-tool init による設定が終わったら再度ビルドして combined.bin をボードに書き込んでおきます。

Wi-Fi アップデートの実施

 ここまででリモートアップデートの準備ができましたので、実際にリモートアップデートを実施してみます。まずアプリケーションのソースに何らかの変更を加えたらこれまで同様にビルドしておきます。ただ今回使用するバイナリは combined.bin ではなく、ブートローダを含まない bin になりますので、今回の例であれば mbed-connect-cloud-application.bin になります。

$ mbed compile -t GCC_ARM -m UBLOX_EVK_ODIN_W2
〜〜〜中略〜〜〜
Image: ./BUILD/UBLOX_EVK_ODIN_W2/GCC_ARM/mbed-connect-cloud-application.bin

 ビルドしたら manifest-tool prepare コマンドを実行して、マニフェストファイルの生成と、バイナリとマニフェストファイルのアップロードを行います。

$ manifest-tool update prepare -p ./BUILD/UBLOX_EVK_ODIN_W2/GCC_ARM/mbed-connect-cloud-application.bin -o myUpdate

 これで Mbed Cloud にマニエストファイルとアップデート用のバイナリファイルがアップロードされましたので、 Mbed Cloud のコンソールからキャンペーンの設定を行いますが、アップデートの配布対象デバイスを指定するためにはデバイスフィルタを作成しておく必要がありますので、下記メニューから作成しておきます。(詳細は割愛)

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

 デバイスフィルタを作成したら Firmware update のメニューからキャンペーンを作成します。

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

 次に表示されるフォームで、先ほど manifest-tool update でアップロードされたマニフェストと、配布対象デバイスの条件として事前に作成したデバイスフィルタを選択し、任意のキャンペーン名を入力して Save します。

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

 するとキャンペーンが作成され、内容が表示されます。

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

 この状態ではまだキャンペーンは開始されていませんので、 Start をクリックするとキャンペーンが開始し、アップデートがスタートします。

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

 これでデバイス側が正しくネットワークに接続して稼働していれば、アップデートが行われます。例えばワークショップ時のサンプルコードではシリアル出力で様子を確認していると下記のようにアップデートが行われたことがわかりました。

Firmware download requested
Authorization granted
Downloading firmware...
Downloading: [-                                                 ] 0 %Temperature: 23.74C!!!!
Downloading: [+/                                                ] 2 %Temperature: 23.74C!!!!
Downloading: [++-                                               ] 4 %Temperature: 23.77C!!!!
Downloading: [+++-                                              ] 6 %Temperature: 23.77C!!!!
〜〜〜中略〜〜〜
Downloading: [++++++++++++++++++++++++++++++++++++++++++++++-   ] 92 %Temperature: 23.77C!!!!
Downloading: [+++++++++++++++++++++++++++++++++++++++++++++++-  ] 95 %Temperature: 23.77C!!!!
Downloading: [++++++++++++++++++++++++++++++++++++++++++++++++\ ] 97 %Temperature: 23.78C!!!!
Downloading: [++++++++++++++++++++++++++++++++++++++++++++++++++] 100 %
Download completed
Firmware install requested
Authorization granted
Booting up...
             Bootloader starting, formatting? 0
                                               [BOOT] mbed Bootloader
[BOOT] ARM: 0000000000000000000000000000000000000000
[BOOT] OEM: 0000000000000000000000000000000000000000
[BOOT] Layout: 0 801A75C
[BOOT] Active firmware integrity check:
[BOOT] [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]
[BOOT] SHA256: 0950D54B7D7CB0957BB3AF342E390D5BEBDF3847E88E291B76F8XXXXXXXXXXXX
[BOOT] Version: 1513261526
[BOOT] Slot 0 firmware integrity check:
[BOOT] [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]
[BOOT] SHA256: 0C596036D5DD603389852A9D7BD18C4BE5743FDCCFC30BD465F3XXXXXXXXXXXX
[BOOT] Version: 1513262444
[BOOT] Slot 1 is empty
[BOOT] Update active firmware using slot 0:
[BOOT] [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]
[BOOT] Verify new active firmware:
[BOOT] [++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]
[BOOT] New active firmware is valid
[BOOT] Application's start address: 0x8020400
[BOOT] Application's jump address: 0x805FB05
[BOOT] Application's stack address: 0x20030000
[BOOT] Forwarding to application...

 キャンペーンの進捗状況はキャンペーン一覧の Active タブから確認できます。

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

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

まとめ

 デバイスを LWM2M で管理するという点では SORACOM Inventory と同じようなイメージを持ちました。Firmware のアップデートについては LWM2M でもできそうな感じではありますが、プラットフォームでサポートしていて複数端末へのアップデートの配布や状況の確認等もコンソールからの設定で行えるのは便利そうに思いました。Mbed Cloud はまだ誰でも利用できるわけではありませんが、どんな感じでデバイス管理を行えるかのイメージが掴めたのは良かったです。

Alexa Skills Kit + AWS IoT + Raspberry Pi + 赤外線LED でテレビリモコンを作る

 Amazon Echo や Google Home では Fire TV や Chromecast 等と組み合わせることで音声でテレビを操作することができるようになりますが、今回は Amazon Echo と Raspberry Pi を連携させ、赤外線LEDなどと組み合わせることでテレビを操作してみたいと思います。

全体構成

 今回の全体の構成は下記の図のようになります。Alexa Skills Kit でカスタムスキルを実装して Amazon Echo から呼び出し、 Fulfillment として Lambda Function を実装してそこから AWS IoT の Shadow の更新リクエストを投げます。Raspberry Pi 上では AWS IoT に MQTT で Subscribe する処理を稼働させておき、Echo からのリクエストで Shadow が更新されたのを検知したら赤外線LEDから赤外線を送信してテレビを操作するという流れです。

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

カスタムスキルの実装

 ではまずはカスタムスキルを実装してみます。 Alexa Skills Kit で下記の内容でスキルを作成します。

スキル名:テレビリモコン
呼び出し名:テレビリモコン

 今回はひとまずできるだけシンプルに一通り動くようにしてみたいと思いますので、テレビのON/OFFのみ操作するようにします。 Alexa へのリクエストとしては「テレビリモコンでテレビをつけて」「テレビリモコンでテレビを消して」の2種類のみを受け取ります。カスタムインテントを一つ作ってスロットで ON/OFF のリクエストを切り分けても良いのですが、少し複雑になってしまうので、今回はスロットは使わずにそれぞれのリクエストに対応するカスタムインテントを定義しておきます。

インテントスキーマ

{
  "intents": [
    { "intent": "TVPowerOnIntent" },
    { "intent": "TVPowerOffIntent" },
    { "intent": "AMAZON.HelpIntent" },
    { "intent": "AMAZON.StopIntent" },
    { "intent": "AMAZON.CancelIntent" }
  ]
}

 サンプル発話は前述の通り2つだけ定義しておきます。一通り動くようになったら他のバリエーションにも対応したいと思います。下記のように定義することで、「テレビリモコンでテレビをつけて」と言った時には TVPowerOnIntent が起動し、「テレビリモコンでテレビを消して」と言った時には TVPowerOffIntent が起動することになります。

サンプル発話

TVPowerOnIntent テレビをつけて
TVPowerOffIntent テレビを消して

 Fulfillment としての Lambda Function は下記のように実装します。

# -*- coding: utf-8 -*-
from __future__ import print_function
import boto3
import json

# --------------- Helpers that build all of the responses ----------------------

def build_speechlet_response(output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }

def build_response(speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': {},
        'response': speechlet_response
    }

# --------------- Functions that control the skill's behavior ------------------

def get_welcome_response():
    speech_output = "テレビを操作するには、「テレビリモコンでテレビをつけて」、" \
                    "または、「テレビリモコンでテレビを消して」、と言ってください。"
    reprompt_text = None
    should_end_session = True
    return build_response(build_speechlet_response(
        speech_output, reprompt_text, should_end_session))

def handle_session_end_request():
    speech_output = None
    reprompt_text = None
    should_end_session = True
    return build_response(build_speechlet_response(
        speech_output, reprompt_text, should_end_session))

def publish(power):
    client = boto3.client('iot-data')
    response = client.update_thing_shadow(
        thingName='home_tv',
        payload=json.dumps({"state":{"desired": {"power": power}}})
    )

def turn_tv_power(power, session):
    should_end_session = True

    publish(power)
    
    speech_output = "テレビの電源を" + power + "にしました。"
    reprompt_text = None
    return build_response(build_speechlet_response(
        speech_output, reprompt_text, should_end_session))

# --------------- Events ------------------

def on_session_started(session_started_request, session):
    print("on_session_started requestId=" + session_started_request['requestId']
          + ", sessionId=" + session['sessionId'])

def on_launch(launch_request, session):
    print("on_launch requestId=" + launch_request['requestId'] +
          ", sessionId=" + session['sessionId'])
    # Dispatch to your skill's launch
    return get_welcome_response()

def on_intent(intent_request, session):
    print("on_intent requestId=" + intent_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']

    if intent_name == "TVPowerOnIntent":
        return turn_tv_power('on', session)
    elif intent_name == "TVPowerOffIntent":
        return turn_tv_power('off', session)
    elif intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    elif intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")

def on_session_ended(session_ended_request, session):
    print("on_session_ended requestId=" + session_ended_request['requestId'] +
          ", sessionId=" + session['sessionId'])

# --------------- Main handler ------------------

def lambda_handler(event, context):
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']},
                           event['session'])

    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'], event['session'])
    elif event['request']['type'] == "SessionEndedRequest":
        return on_session_ended(event['request'], event['session'])

 処理の内容はインテントの種別で切り分けています。

    if intent_name == "TVPowerOnIntent":
        return turn_tv_power('on', session)
    elif intent_name == "TVPowerOffIntent":
        return turn_tv_power('off', session)
    elif intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    elif intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")

 turn_tv_power メソッドの中で publish メソッドを呼び出し、その中で AWS SDK を使って AWS IoT の Shadow の更新リクエストを投げています。今回は Shadow の内容としてはテレビの ON/OFF のステータスのみを持たせています。

def publish(power):
    client = boto3.client('iot-data')
    response = client.update_thing_shadow(
        thingName='home_tv',
        payload=json.dumps({"state":{"desired": {"power": power}}})
    )

 テストをして問題なければ Alexa Skills Kit のエンドポイントの設定で上記の Lambda Function の ARN を指定します。

 カスタムスキルの実装については前回も書いてますので、よろしければご覧ください。

blog.akanumahiroaki.com

Raspberry Pi と電子部品の配線

 今度はテレビを操作する側を実装していきます。まずは Raspberry Pi と赤外線LED等を下記の図のように配線します。

f:id:akanuma-hiroaki:20171206081102p:plain:w300:left

 今回使っているパーツは下記の3つです。

 ・赤外線受信モジュール
 ・赤外線LED
 ・NPN型トランジスタ

 これらを抵抗とジャンパーコードで接続します。

 赤外線受信モジュールはテレビのリモコンの赤外線を受信してパターンを記録するために使います。今回使用したモジュールのピンは左から Output, GND, VCC となっているので、 Output を GPIO17 に接続し、 GND と VCC をそれぞれ GND と 3V 出力に接続します。

 そして記録したパターンで赤外線を送信するために赤外線LEDを使うのですが、通常のGPIOで出力される電圧では小さいため、トランジスタを使用して Raspberry Pi の 5V の電源を増幅して使います。今回使用したトランジスタはNPN型で、ピンは左から Emitter, Collector, Base です。 赤外線LEDのアノードを 5V 出力に接続し、 4.7Ωの抵抗を介して Collector に接続します。Emitter は GND に接続します。 Base は 1kΩの抵抗を介して GPIO18 に接続します。

www.aitendo.com

www.aitendo.com

www.aitendo.com

LIRC で赤外線パターンを記録する

 今回はちょっと手抜きな感じもありますが、 赤外線の送信に LIRC を使って動かしてみます。

www.lirc.org

 まず下記コマンドで LIRC をインストールします。

$ sudo apt-get install lirc

 起動時に LIRC モジュールを起動するように、 /boot/config.txt に下記を追記します。

dtoverlay=lirc-rpi,gpio_in_pin=17,gpio_out_pin=18

 追記後に再起動すると下記のようにデバイスファイルが現れます。

pi@raspberrypi:~ $ ls -l /dev/lirc0 
crw-rw---- 1 root video 243, 0 Dec  5 20:52 /dev/lirc0

 そしてテレビのリモコンの赤外線のパターンを記録させます。 irrecord コマンドを使うのですが、そのためには一度 LIRC を停止します。

pi@raspberrypi:~ $ sudo /etc/init.d/lirc stop
Stopping lirc (via systemctl): lirc.service.

 停止させたら irrecord コマンドで赤外線パターンを記録し、 lircd.conf というファイルに出力します。

pi@raspberrypi:~ $ irrecord -n -d /dev/lirc0 lircd.conf
〜〜〜中略〜〜〜
Please send the finished config files to <lirc@bartelmus.de> so that I
can make them available to others. Don't forget to put all information
that you can get about the remote control in the header of the file.

Press RETURN to continue. ← Enter押下

Now start pressing buttons on your remote control.

It is very important that you press many different buttons and hold them
down for approximately one second. Each button should generate at least one
dot but in no case more than ten dots of output.
Don't stop pressing buttons until two lines of dots (2x80) have been
generated.

Press RETURN now to start recording. ← Enter押下
↓テレビのリモコンの色々なボタンを1秒以上押し続ける。受信できている間はドットが表示されていく
................................................................................
Found gap: 74690
Please keep on pressing buttons like described above.
................................................................................
Suspicious data length: 99.
Found possible header: 3461 1746
Header is not being repeated.
Found trail pulse: 422
No repeat code found.
Signals are space encoded.
Signal length is 48
Now enter the names for the buttons.

Please enter the name for the next button (press <ENTER> to finish recording)
power ← これから押すボタン(power)の名前を入力してEnter

Now hold down button "power". ← 電源ボタンを押す

Please enter the name for the next button (press <ENTER> to finish recording)
ch1 ← 1チャンネルボタンの名前を入力してEnter

Now hold down button "ch1". ← 1チャンネルボタンを押す

Please enter the name for the next button (press <ENTER> to finish recording)
ch2 ← 2チャンネルボタンの名前を入力してEnter

Now hold down button "ch2". ← 2チャンネルボタンを押す

Please enter the name for the next button (press <ENTER> to finish recording) ← 入力を終了するために空Enter

Checking for toggle bit mask.
Please press an arbitrary button repeatedly as fast as possible.
Make sure you keep pressing the SAME button and that you DON'T HOLD
the button down!.
If you can't see any dots appear, then wait a bit between button presses.

Press RETURN to continue. ← Enter押下
........................ ← 任意の一つのボタンを連打する
No toggle bit mask found.
Successfully written config file.

 出力された lircd.conf ファイルの中身は下記のようになっています。

pi@raspberrypi:~ $ cat lircd.conf

# Please make this file available to others
# by sending it to <lirc@bartelmus.de>
#
# this config file was automatically generated
# using lirc-0.9.0-pre1(default) on Thu Dec  7 14:49:17 2017
#
# contributed by 
#
# brand:                       lircd.conf
# model no. of remote control: 
# devices being controlled by this remote:
#

begin remote

  name  lircd.conf
  bits           24
  flags SPACE_ENC|NO_HEAD_REP
  eps            30
  aeps          100

  header       3461  1746
  one           424  1309
  zero          424   442
  ptrail        422
  pre_data_bits   24
  pre_data       0x400401
  gap          74690
  min_repeat      5
#  suppress_repeat 5
#  uncomment to suppress unwanted repeats
  toggle_bit_mask 0x0

      begin codes
          power                    0x00BCBD
          ch1                      0x900293
          ch2                      0x908213
      end codes

end remote

 これを /etc/lirc ディレクトリにコピーします。

$ sudo cp lircd.conf /etc/lirc/.

 続いて設定ファイル /etc/lirc/hardware.conf を編集します。編集前のファイルとの差分は下記の通りです。

pi@raspberrypi:~ $ diff /etc/lirc/hardware.conf.bak /etc/lirc/hardware.conf
4c4
< LIRCD_ARGS=""
---
> LIRCD_ARGS="--uinput"
16c16
< DRIVER="UNCONFIGURED"
---
> DRIVER="default"
18,19c18,19
< DEVICE=""
< MODULES=""
---
> DEVICE="/dev/lirc0"
> MODULES="lirc_rpi"

 ここまでで設定は完了なので、 LIRC を再起動します。

pi@raspberrypi:~ $ sudo /etc/init.d/lirc restart
Restarting lirc (via systemctl): lirc.service.

AWS IoT Shadow の更新情報を受け取ってテレビを操作する

 それでは赤外線LEDからテレビに赤外線を送信する部分の処理を実装します。 Ruby のコード中から Open3 を使って LIRC の irsend コマンドを呼んでいます。

require 'bundler/setup'
require 'pi_piper'
require 'open3'
require 'logger'

class IR_Transmitter
  LOG_FILE = 'logs/ir_transmitter.log'

  def initialize
    @led_pin = PiPiper::Pin.new(pin: 18, direction: :out)
    @log = Logger.new(LOG_FILE)
  end

  def transmit
    send_signal
  end

  def send_signal
    begin
      result = Open3.capture3('irsend SEND_START TV power')
      @log.debug("SEND_START #{result.inspect}")

      sleep 2

      result = Open3.capture3('irsend SEND_STOP TV power')
      @log.debug("SEND_STOP #{result.inspect}")
    rescue => e
      @log.error(e.backtrace.join("\n"))
    ensure
      Open3.capture3('irsend SEND_STOP TV power')
    end
  end
end

 今回使っているテレビのリモコンは ON と OFF のボタンは分かれていないので、 いずれの場合も同じように赤外線を2秒送信してストップしています。

 次に、 AWS IoT に Subscribe して Shadow の更新を受け取る処理の実装です。下記内容を subscriber.rb として保存します。AWS IoT に MQTT で接続し、 Shadow 更新時の差分情報が配信される delta トピックに Subscribe して待ち受け、差分情報を受け取った時にその内容から ON/OFF の state を抜き出して前述の赤外線送信処理を実行します。

require 'bundler/setup'
require 'mqtt'
require 'json'
require 'open3'
require './ir_transmitter.rb'

AWS_IOT_URL = 'xxxxxxxxxxxxxx.iot.ap-northeast-1.amazonaws.com'
AWS_IOT_PORT = 8883
TOPIC = '$aws/things/home_tv/shadow/update'
DELTA_TOPIC = "#{TOPIC}/delta"

class Subscriber
  LOG_FILE = 'logs/subscriber.log'

  def initialize
    @ir_transmitter = IR_Transmitter.new
    @log = Logger.new(LOG_FILE)
  end

  def statement(power:)
    {
      state: {
        reported: {
          power: power,
        }
      }
    }
  end

  def subscribe
    MQTT::Client.connect(host: AWS_IOT_URL, port: AWS_IOT_PORT, ssl: true, cert_file: 'home_tv-certificate.pem.crt', key_file: 'home_tv-private.pem.key', ca_file: 'root-CA.crt') do |client|
      initial_state = statement(power: 'off').to_json
      client.publish(TOPIC, initial_state)
      @log.info("Published initial statement: #{initial_state}")

      client.subscribe(DELTA_TOPIC)
      @log.info("Subscribed to the topic: #{DELTA_TOPIC}")

      client.get do |topic, json|
        state = JSON.parse(json)['state']
        power = state['power']

        @ir_transmitter.transmit

        reported_state = statement(power: power).to_json

        client.publish(TOPIC, reported_state)
        @log.info("Reported state: #{reported_state}")
      end
    end
  end
end

if $PROGRAM_NAME == __FILE__
  subscriber = Subscriber.new
  subscriber.subscribe
end

 AWS IoT については以前の記事でも書いてますので、詳細はそちらをご覧ください。

blog.akanumahiroaki.com

動作確認

 ここまでで一通りの実装ができたので、 Raspberry Pi 上で Subscriber を起動しておきます。

$ sudo bundle exec ruby subscriber.rb

 また、カスタムスキルが Alexa の Your Skills のリストに表示されていることを確認します。

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

 この状態で Alexa に「テレビリモコンでテレビをつけて」と言うと、赤外線LEDから赤外線が送信され、Alexa が「テレビの電源をONにしました」と返答してくれます。また、「テレビリモコンでテレビを消して」と言うと、同様に赤外線LEDから赤外線が送信され、Alexa が「テレビの電源をOFFにしました」と返答してくれます。

課題

 今回使用しているテレビのリモコンは ON/OFF のボタンが同一で、実際にテレビの ON/OFF の状態がどうなっているかは検知できていないので、テレビが ON/OFF どちらの状態から始めるかで逆の状態になってしまう可能性があります。また、赤外線が正しく受信されて ON/OFF が切り替わったかも検知できていないので、何らかの方法で実際のテレビのステータスを検知することが必要です。また、赤外線は指向性が強いのと、今回の装置ぐらいだと結構近受信部に近づかないと届かないので、設置場所には工夫が必要そうです。

まとめ

 赤外線はリモコンの各処理のパターンを記憶させる手間はかかりますが、色々な処理を自動化したりリモートで実行させたりすることができそうなので、今回の内容をベースに外出先からエアコンを操作できるようにしたりしてみたいと思います。もちろんもともと対応している家電があればそれがお手軽ですが、自前の装置で操作できるというのはやっぱり面白いですね。

Alexa Skills Kit でスロットを使ったスキルを実装する

 前回は Alexa Skills Kit で、起動のリクエストを受け付けると決まった処理をしてレスポンスを返すだけのシンプルなスキルを実装してみましたが、今回はユーザからの入力値を使って処理をするスキルを実装してみたいと思います。具体的には、前回は順番決めスキルの中で固定で持っていた対象者リストを、ユーザから入力された名前を使用するように変更してみます。

対話モデルの変更

 まずは対話モデルを変更します。 Alexa Skills Kit では、ユーザからの入力値はスロットという引数で扱うことができますので、インテントスキーマでスロットの利用を定義します。

{
  "intents": [
    {
      "intent": "DecideOrderIntent",
      "slots": [
        { "name": "NameA", "type": "AMAZON.FirstName" },
        { "name": "NameB", "type": "AMAZON.FirstName" },
        { "name": "NameC", "type": "AMAZON.FirstName" },
        { "name": "NameD", "type": "AMAZON.FirstName" }
      ]
    },
    { "intent": "AMAZON.HelpIntent" },
    { "intent": "AMAZON.StopIntent" },
    { "intent": "AMAZON.CancelIntent" }
  ]
}

 今回の実装では、対象者を4人まで指定できるようにしたいと思いますので、カスタムインテントの DecideOrderIntent にスロットを4つ定義しています。スロットを定義する際にはスロットタイプも指定する必要があり、独自のタイプを定義することもできますが、あらかじめ用意されている標準スロットタイプで該当するものがあればそちらを使った方が何かと便利です。今回は名前を扱うためのスロットなので、標準ライブラリの AMAZON.FirstName を使用しています。

developer.amazon.com

 次に、インテントスキーマで定義したスロットを使えるように、サンプル発話を変更します。

DecideOrderIntent 順番決め
DecideOrderIntent 順番決めて
DecideOrderIntent 順番を決めて
DecideOrderIntent 順番決めを開いて
DecideOrderIntent 誰の順番
DecideOrderIntent 誰の番
DecideOrderIntent 誰が先
DecideOrderIntent {NameA} と {NameB} の順番を決めて
DecideOrderIntent {NameA} と {NameB} と {NameC} の順番を決めて
DecideOrderIntent {NameA} と {NameB} と {NameC} と {NameD} の順番を決めて
DecideOrderIntent {NameA} と {NameB} の順番
DecideOrderIntent {NameA} と {NameB} と {NameC} の順番
DecideOrderIntent {NameA} と {NameB} と {NameC} と {NameD} の順番
DecideOrderIntent {NameA} と {NameB}
DecideOrderIntent {NameA} と {NameB} と {NameC}
DecideOrderIntent {NameA} と {NameB} と {NameC} と {NameD}

 8行目以降が今回追加した内容です。スロットはサンプル発話の中にプレースホルダーのような形で配置します。今回の4つのスロットは全て人の名前で、2人〜4人で可変なので、リスト形式で受け取れると良いのですが、それはできないようだったので、2人、3人、4人のそれぞれのパターンを定義しています。ちなみにスロット名とその前後のテキストの間に半角スペースを入れないと対話モデルの保存時にパースエラーになってしまいました。

 基本的には対話モデルの変更はここまででOKですが、今回使用している標準スロットタイプの AMAZON.FirstName は日本語を話すユーザが一般的に使用する数千個の名前を集めたものなので、マイナーな名前やニックネームを使いたい場合には候補を追加して拡張することができます。そのためには対話モデルの設定画面のカスタムスロットタイプの項目で下記のように入力します。(下記の例で使っている名前はあらかじめ含まれているとは思いますが。。)

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

 更新 ボタンをクリックすると値が登録され、下記のような表示に変わります。

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

 複数の標準スロットタイプを使っていて他のスロットタイプも拡張したい場合には スロットタイプの追加 をクリックするとさらにフォームが表示されるので、追加で登録することができます。

 ここまでの内容を 保存 して対話モデルの変更は終了です。

Lambda ファンクションの変更

 スロットの内容を使えるように、 Lambda ファンクションを変更します。変更後の実装内容は下記のようになります。

# -*- coding: utf-8 -*-
from __future__ import print_function
from random import shuffle

# --------------- Helpers that build all of the responses ----------------------

def build_speechlet_response_with_card(title, output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'card': {
            'type': 'Simple',
            'title': title,
            'content': output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }

def build_speechlet_response_without_card(output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }

def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }

# --------------- Functions that control the skill's behavior ------------------

def decide_order(names):
    shuffle(names)
    return "今回は、" + '、'.join(names) + "の順番です。"

def get_welcome_response():
    session_attributes = {}
    speech_output = '対象の名前を四人まで言ってください'
    reprompt_text = '聞き取れませんでした。' + speech_output

    should_end_session = False
    return build_response(session_attributes, build_speechlet_response_without_card(
        speech_output, reprompt_text, should_end_session))

def get_help_response():
    session_attributes = {}
    speech_output = 'このスキルでは、四人までの対象者の順番をシャッフルして決定します。' + \
                    '「誰と誰と誰」、という形で、対象の名前を四人まで言ってください'
    reprompt_text = '聞き取れませんでした。対象の名前を四人まで言ってください'

    should_end_session = False
    return build_response(session_attributes, build_speechlet_response_without_card(
        speech_output, reprompt_text, should_end_session))

def handle_session_end_request():
    speech_output = None
    reprompt_text = None
    should_end_session = True
    return build_response({}, build_speechlet_response_without_card(
        speech_output, reprompt_text, should_end_session))

def get_order(intent, session):
    session_attributes = {}

    slots = intent['slots']
    
    if 'NameA' not in slots or 'NameB' not in slots or 'NameC' not in slots or 'NameD' not in slots:
        speech_output = '名前がわかりませんでした。もう一度言ってください。'
        reprompt_text = speech_output
        should_end_session = False

        return build_response(session_attributes, build_speechlet_response_without_card(
            speech_output, reprompt_text, should_end_session))

    else:
        reprompt_text = None
        should_end_session = True
        card_title = "順番を決めました"
        names = []

        if 'value' in intent['slots']['NameA']:
            names.append(intent['slots']['NameA']['value'])
        if 'value' in intent['slots']['NameB']:
            names.append(intent['slots']['NameB']['value'])
        if 'value' in intent['slots']['NameC']:
            names.append(intent['slots']['NameC']['value'])
        if 'value' in intent['slots']['NameD']:
            names.append(intent['slots']['NameD']['value'])

        speech_output = decide_order(names)

        return build_response(session_attributes, build_speechlet_response_with_card(
            card_title, speech_output, reprompt_text, should_end_session))

# --------------- Events ------------------

def on_session_started(session_started_request, session):
    print("on_session_started requestId=" + session_started_request['requestId']
          + ", sessionId=" + session['sessionId'])

def on_launch(launch_request, session):
    print("on_launch requestId=" + launch_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    return get_welcome_response()

def on_intent(intent_request, session):
    print("on_intent requestId=" + intent_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']

    if intent_name == "DecideOrderIntent":
        return get_order(intent, session)
    elif intent_name == "AMAZON.HelpIntent":
        return get_help_response()
    elif intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")

def on_session_ended(session_ended_request, session):
    print("on_session_ended requestId=" + session_ended_request['requestId'] +
          ", sessionId=" + session['sessionId'])

# --------------- Main handler ------------------

def lambda_handler(event, context):
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']},
                           event['session'])

    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'], event['session'])
    elif event['request']['type'] == "SessionEndedRequest":
        return on_session_ended(event['request'], event['session'])

 主に変更したのは get_order() メソッドです。

def get_order(intent, session):
    session_attributes = {}

    slots = intent['slots']
    
    if 'NameA' not in slots or 'NameB' not in slots or 'NameC' not in slots or 'NameD' not in slots:
        speech_output = '名前がわかりませんでした。もう一度言ってください。'
        reprompt_text = speech_output
        should_end_session = False

        return build_response(session_attributes, build_speechlet_response_without_card(
            speech_output, reprompt_text, should_end_session))

    else:
        reprompt_text = None
        should_end_session = True
        card_title = "順番を決めました"
        names = []

        if 'value' in intent['slots']['NameA']:
            names.append(intent['slots']['NameA']['value'])
        if 'value' in intent['slots']['NameB']:
            names.append(intent['slots']['NameB']['value'])
        if 'value' in intent['slots']['NameC']:
            names.append(intent['slots']['NameC']['value'])
        if 'value' in intent['slots']['NameD']:
            names.append(intent['slots']['NameD']['value'])

        speech_output = decide_order(names)

        return build_response(session_attributes, build_speechlet_response_with_card(
            card_title, speech_output, reprompt_text, should_end_session))

 Lambda ファンクションに送信される JSON の内容で、スロットに関する部分を抜粋すると下記のような形になります。

{
  "request": {
    "type": "IntentRequest",
    "requestId": "EdwRequestId.2ea90888-5a5f-4fa1-982d-xxxxxxxxxxxx",
    "intent": {
      "name": "DecideOrderIntent",
      "slots": {
        "NameC": {
          "name": "NameC",
          "value": "じろう"
        },
        "NameD": {
          "name": "NameD"
        },
        "NameA": {
          "name": "NameA",
          "value": "たろう"
        },
        "NameB": {
          "name": "NameB",
          "value": "はなこ"
        }
      }
    },
    "locale": "ja-JP",
    "timestamp": "2017-11-23T07:30:42Z"
  },
}

 get_order() メソッドには intent 以下を渡していますので、そこから slots を取り出し、NameA から NameD までの値を取り出しています。

 今回は対象者数は可変なので、値が設定されていないスロットもありますが、その場合にも上記サンプルの NameD のように、 value の項目がないだけで、 name の項目は入った状態で渡されますので、NameA から NameD のいずれかのキー自体が存在しない場合には、名前を読み取れていないとしてもう一度入力を促すようにしています。

 全てのキーがあった場合は、その中の value のキーがある項目の値を対象者リストに追加して、 decide_order() メソッドに渡しています。

 decide_order() メソッドでは渡された対象者リストをシャッフルしてレスポンスを返しています。

def decide_order(names):
    shuffle(names)
    return "今回は、" + '、'.join(names) + "の順番です。"

 また、ユーザからの対象者名の入力を受け付けるために、スキルの起動リクエストだけを受け取った場合にはユーザに対象者名の入力を促すようにし、セッションを継続するようにしています。

def get_welcome_response():
    session_attributes = {}
    speech_output = '対象の名前を四人まで言ってください'
    reprompt_text = '聞き取れませんでした。' + speech_output

    should_end_session = False
    return build_response(session_attributes, build_speechlet_response_without_card(
        speech_output, reprompt_text, should_end_session))

 そのほかにも AMAZON.HelpIntent を受け取った時の説明内容や、Alexa アプリに表示されるカードの内容などを少し整理しています。

動作確認

 ここまででひとまず実装としては完了なので、動作確認をしてみます。前回の記事でやったようにシミュレータを使ってテストをすることもできますが、Amazon Echo などの実機があれば、実機でテストをすることも可能です。Alexa の管理画面もしくは Alexa アプリでスキルストアにアクセスし、右上の '''Your Skills''' をクリックします。

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

 すると追加済みのスキル一覧ページが表示されますので、その中に開発中のスキルが表示されて入れば実機でのテストが可能な状態です。

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

 この状態で例えば下記のようなやりとりができれば想定通り実装できています。

ユーザ 「アレクサ、順番決めを開いて」 アレクサ 「対象の名前を四人まで言ってください。」 ユーザ「太郎と花子と二郎」 アレクサ「今回の順番は、花子、二郎、一郎です。」

まとめ

 今回はユーザからの入力内容を扱うようにしたとはいえまだまだシンプルな内容です。それでもやはり固定の処理を行うだけの時と比べると、正常ケースだけでも複数のパターンを考慮する必要がありますし、エラーケースや中断のケースなども含めるとかなり考えることが増えています。また、Webやスマートフォンアプリと違い、入力内容(発話内容)を正しく解釈できるかどうかという点も不安定さはありますので、インタフェース設計は音声入力の特徴をよく考慮した上で設計することが重要と感じました。

Alexa Skills Kit でシンプルなカスタムスキルを実装

 前回 Amazon Echo Dot の初期設定と既存のスキルを使ってみるところだけやりましたが、今回は自作のスキル(カスタムスキル)を実装してみます。Alexa では対話形式で複雑な処理を行うスキルも実装できますが、まず今回はシンプルに、スキルを起動したら結果を返して終了するという最もシンプルなパターンのカスタムスキルを実装してみます。

 うちには小学生の子どもが二人いるのですが、ゲームの順番などでしょっちゅう喧嘩しているので、 Alexa に順番を決めてもらうスキルを作成してみます。(教育的には話し合って解決できるようにするべきという点は一旦置いておきます。)

 ひとまず今回はシミュレータで動作確認ができるところまでをやってみたいと思います。

開発の流れ

 カスタムスキルを作成するには Alexa Skills Kit(ASK)を利用します。Alexa Skills Kit での開発については下記で公式のドキュメントが公開されています。

developer.amazon.com

 スキル開発の流れとしては、上記ドキュメントで下記画像のように紹介されています。

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

 また、公式ドキュメントの一部として、クラスメソッドが作成しているトレーニングドキュメントもあるようです。

developer.amazon.com

 開発工程での主な流れとしては下記のようになるかと思います。

  1. 対話モデルの作成

  2. Lambda でバックエンドの処理を作成して対話モデルと紐付け

  3. シミュレータ or 実機でテスト

 Amazon Developer Account や AWS Account の作成方法は割愛させていただきますので、アカウント登録が終わっている前提で、それぞれ進めてみたいと思います。

対話モデルの作成

 まずはスキルを作成して対話モデルを作成していきます。下記リンク先にアクセスして開発者コンソールのダッシュボードを開きます。

Amazon Developer Sign In

 開発者コンソールのダッシュボードは下記のような画面になります。

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

 画面上部の ALEXA メニューをクリックして Alexa のダッシュボードを開きます。

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

 Alexa の要素としては Alexa Skills Kit(ASK) と Alexa Voice Service(AVS) があり、今回は Alexa Skills Kit を選択します。ちなみに Alexa Voice Service は音声を認識してテキストに変換したり、テキストの内容を音声に変換して発話するために利用するもので、自前でハードウェアに Alexa を組み込んでスマートスピーカーを作る際などに利用します。

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

 初めてスキルを作成する場合はスキル一覧には何もありませんが、作成済みのスキルがある場合は上記のように一覧に表示されます。今回は新たにスキルを作るために 新しいスキルを追加する をクリックします。

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

 スキルの作成画面に遷移しますので、今回は下記の内容で入力・選択します。

  • スキルの種類:カスタム対話モデル

  • 言語:Japanese

  • スキル名:順番決め

  • 呼び出し名:順番決め

 その他の項目は今回はデフォルトのままにしておきます。入力できたら画面下部の 保存 ボタンをクリックします。

f:id:akanuma-hiroaki:20171122223615j:plain

 するとスキルが作成されますので、 次へ ボタンをクリックして対話モデル作成画面へ進みます。

f:id:akanuma-hiroaki:20171122223840j:plain

 上記は対話モデル作成画面での入力後の状態です。

 まずインテントスキーマは Alexa からバックエンドの Lambda ファンクションへ送られるインテントの種類を定義します。今回は下記のように定義します。

{ "intents": [
    { "intent": "DecideOrderIntent" },
    { "intent": "AMAZON.HelpIntent" },
    { "intent": "AMAZON.StopIntent" },
    { "intent": "AMAZON.CancelIntent" }
]}

 今回実装するカスタムスキル用のインテントとして DecideOrderIntent を定義しています。また、それ以外にビルトインインテントを3種類使用します。

 今回のカスタムスキルでは特にユーザから情報を提供する必要はないので、カスタムスロットタイプはブランクのままにしておきます。将来的にはユーザから順番決めの候補者名を提供するようにしたいので、その際にはカスタムスロットタイプを定義することになるかと思います。

 また、サンプル発話は下記のように定義します。

DecideOrderIntent 順番決め
DecideOrderIntent 順番決めて
DecideOrderIntent 順番を決めて
DecideOrderIntent 順番決めを開いて
DecideOrderIntent 誰の順番
DecideOrderIntent 誰の番
DecideOrderIntent 誰が先

 サンプル発話にはユーザがこのスキルを呼び出したいときに使う可能性のある発話内容をリストアップしておきます。

 ここまで入力したら 次へ ボタンをクリックしてエンドポイント等の設定画面へ進みます。ここでスキルのコンソールは一旦置いておいて、 Lambda ファンクションの実装に移ります。

エンドポイントの Lambda 実装

 Alexa Skills Kit では、入力された音声の内容に応じて処理を行うロジックとして、 AWS Lambda のファンクションを使うか、 Web API を呼び出すかを指定できます。標準は Lambda なので、今回も Lambda のファンクションとして実装します。Alexa Skills Kit のエンドポイントとしての Lambda 実装についてのリファレンスは下記になります。

developer.amazon.com

 Alexa Skills Kit でのエンドポイントとのやりとりは JSON で行われますが、その内容についてのリファレンスは下記になります。

developer.amazon.com

 まずは AWS のコンソールにログインして Lambda のコンソールを表示します。

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

 新たにファンクションを作成するため、 関数の作成 をクリックします。

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

 ベースとなるサンプルを選択します。もちろんファンクションを一から作成しても良いのですが、 Alexa のエンドポイント用の実装サンプルがいくつか公開されていますので、それを変更する形で実装したいと思います。今回は Python での実装サンプルである、「alexa-skills-kit-color-expert-python」を利用します。

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

 基本的な情報としてファンクション名とロールを設定します。私のケースでは既存のロールがあったので 既存のロールを選択 を選択してそれを利用していますが、初めて作成する場合には テンプレートから新しいロールを作成 もしくは カスタムロールの作成 を選択して新しくロールを作成します。

 上記以外はひとまずそのままで、画面下部の 関数の作成 ボタンをクリックするとファンクションが作成されます。

f:id:akanuma-hiroaki:20171122224059j:plain

 私が実行したところ、作成後の画面で上記のようなエラーが表示されました。Alexa Skills Kit のトリガーの作成に失敗したということのようですが、トリガーとしては追加されているようなので、ひとまずこのまま進みます。

 一旦 Lambda コンソールは置いておいて、作成した Lambda ファンクションをスキルに紐づけるために、スキルのコンソールに戻ります。

f:id:akanuma-hiroaki:20171122224320j:plain

 サービスエンドポイントのタイプで「AWS Lambda の ARN」を指定し、Lambda ファンクション作成後の画面の右上に表示されていた ARN をデフォルトに設定します。「エンドポイントの地理的リージョンを設定しますか?」の項目には「いいえ」を指定し、その他はデフォルトのままで 次へ をクリックします。

f:id:akanuma-hiroaki:20171122224612j:plain

 シミュレーターが表示されますので、意図した通りにカスタムスキルを呼び出せるか確認します。エンドポイントにもリクエストが送信されますので、先ほど作成した Lambda ファンクションへも実際にリクエストが送信されることになります。

 サービスシミュレーターのテキストに、カスタムスキルのサンプル発話に登録したものの一つを入力して 順番決めを呼び出す をクリックします。するとリクエストとレスポンスの JSON が下に表示されます。レスポンスの内容は Lambda ファンクションの処理結果になりますが、先ほど作成したファンクションはまだサンプルの内容を変更していませんので、サンプルのレスポンスがそのまま返ってきています。

 それでは Lambda ファンクションを今回のカスタムスキルの意図に沿ったものに変更していきますが、まずはファンクションのテストのためにどんな JSON がリクエストされるのかを確認しておきます。シミュレータの実行結果ではリクエストの JSON は下記のようになっていました。あくまで私が実行した時の例なので、ID等の値は変わってくるかと思いますし一部マスクしていますが、構成は同様になるかと思います。

{
  "session": {
    "new": true,
    "sessionId": "SessionId.7caeec82-30ad-4629-9c96-xxxxxxxxxxxx",
    "application": {
      "applicationId": "amzn1.ask.skill.1f313097-0cb4-4be1-a4d8-xxxxxxxxxxxx"
    },
    "attributes": {},
    "user": {
      "userId": "amzn1.ask.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "EdwRequestId.252e3464-1623-422f-9bf3-xxxxxxxxxxxx",
    "locale": "ja-JP",
    "timestamp": "2017-11-19T22:25:21Z"
  },
  "context": {
    "AudioPlayer": {
      "playerActivity": "IDLE"
    },
    "System": {
      "application": {
        "applicationId": "amzn1.ask.skill.1f313097-0cb4-4be1-a4d8-xxxxxxxxxxxx"
      },
      "user": {
        "userId": "amzn1.ask.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      },
      "device": {
        "supportedInterfaces": {}
      }
    }
  },
  "version": "1.0"
}

 この JSON をそのまま Lambda コンソールからテストイベントとして設定しておきます。

f:id:akanuma-hiroaki:20171122224939j:plain

 そして今回のファンクションの内容は下記のようにしました。とりあえず今回はユーザからの候補者名の入力は受け付けず、固定の二人の名前を実行時に毎回シャッフルして順番を返す単純なものです。

# -*- coding: utf-8 -*-
from __future__ import print_function
from random import shuffle

# --------------- Helpers that build all of the responses ----------------------

def build_speechlet_response(title, output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'card': {
            'type': 'Simple',
            'title': "SessionSpeechlet - " + title,
            'content': "SessionSpeechlet - " + output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }

def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }

# --------------- Functions that control the skill's behavior ------------------

def decide_order():
    candidates = ['太郎君', '花子さん']
    shuffle(candidates)
    return "今回は、" + '、'.join(candidates) + "の順番です。"

def get_welcome_response():
    session_attributes = {}
    card_title = "順番決め"
    speech_output = decide_order()

    should_end_session = True
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, None, should_end_session))

def handle_session_end_request():
    card_title = "順番決め終了"
    speech_output = "良い一日を!"

    should_end_session = True
    return build_response({}, build_speechlet_response(
        card_title, speech_output, None, should_end_session))

def get_order(intent, session):
    session_attributes = {}
    reprompt_text = None

    speech_output = decide_order()
    should_end_session = True

    return build_response(session_attributes, build_speechlet_response(
        intent['name'], speech_output, reprompt_text, should_end_session))

# --------------- Events ------------------

def on_session_started(session_started_request, session):
    print("on_session_started requestId=" + session_started_request['requestId']
          + ", sessionId=" + session['sessionId'])

def on_launch(launch_request, session):
    print("on_launch requestId=" + launch_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    return get_welcome_response()

def on_intent(intent_request, session):
    """ Called when the user specifies an intent for this skill """

    print("on_intent requestId=" + intent_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']

    if intent_name == "DecideOrderIntent":
        return get_order(intent, session)
    elif intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    elif intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")

def on_session_ended(session_ended_request, session):
    print("on_session_ended requestId=" + session_ended_request['requestId'] +
          ", sessionId=" + session['sessionId'])

# --------------- Main handler ------------------

def lambda_handler(event, context):
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']},
                           event['session'])

    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'], event['session'])
    elif event['request']['type'] == "SessionEndedRequest":
        return on_session_ended(event['request'], event['session'])

 lambda_handler() メソッドに渡している event には、先ほど確認したリクエストの内容が入ってきます。

 また、リクエストの種別としては下記3種類を想定していて、処理内容を切り分けています

  • LaunchRequest: スキル起動時

  • IntentRequest: セッション継続中 もしくはスキル起動時に追加情報と一緒に起動した場合

  • SessionEndedRequest: スキル終了時

 今回はユーザからの追加情報は受け付けないので、 LaunchRequest と IntentRequest で基本的には同じ処理をしています。 IntentRequest の処理の内容はインテント名でさらに切り分けて AMAZON.HelpIntent 等についての処理もサンプルにあったものをそのまま残していますが、今回の内容ではスキルの起動以外にユーザの入力を待ち受けることがないため、 DecideOrderIntent 以外の処理は実行されないかと思います。

 ファンクションを保存して再度シミュレータからリクエストを実行すると、下記のようにレスポンスが返るようになります。

f:id:akanuma-hiroaki:20171122225220j:plain

まとめ

 今回はシンプルなカスタムスキル実装ということで、ユーザからはスキル起動の発話だけ受け取って結果を返すという単純なものでしたので、入力値のハンドリング等も特に必要なく、簡単に実装できました。 Lambda からはさらに他のAWSサービスや外部サービスへも連携できますので、アイディア次第で色々なことができそうです。一方でユーザからの入力内容によって処理を変えられるようにもしたいので、次回以降で入力値のハンドリングも行うスキルを実装してみたいと思います。