Alexaでラズパイ3の情報（ＩＰアドレス）を取得する　※手抜き版

# はじめに
以下のようにアレクサを介してラズパイ3からIPアドレスを取得してみました。（※2019/11/21現在で有効な方法です）
　
「アレクサ、ラズパイの情報を開いて」（私）
　↓
「IPアドレスですね？」（アレクサ）
　↓
「はい」（私）
　↓
「IPアドレスはxxx.xxx.xxx.xxxです」（アレクサ）

※この手順はサンプルコードに必要最低限の変更を加えた「手抜き版」です。メソッド名やメンバ名等はサンプルのままです。違和感がぱないです。そこはどうぞ良きに計らってください・・・。

図：本環境の概要

![概要図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155449/1e22b0f4-6d04-5a3e-5ab8-bc8637ea6d26.png)


# 使用したもの
* Echo Dot 第3世代
* ラズパイ3B　(OS: Raspbian Stretch 2019-04-08)  
　→「Raspberry Pi Zero W」でも良さそうです。（[参考](https://github.com/alexa/Alexa-Gadgets-Raspberry-Pi-Samples#prerequisites)）
* Alexa Gadgets Toolkit
* Alexaスキル
* AWS Lambda

# サンプルプログラムの実行環境を作る

[「こちら」](https://qiita.com/youtoy/items/472da6c009a5ee3407ea#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%992-avsalexa-voice-service%E3%81%AE%E9%96%8B%E7%99%BA%E8%80%85%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E3%81%A7%E3%82%AC%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%81%AE%E7%99%BB%E9%8C%B2)の「事前準備2」、「事前準備3」、「カスタムスキルの作成」を見てそのまま実施しました。※「Color Cycler Gadget」を使用します

※[「こちら」](https://raspida.com/connect-rpi3bp-alexagadget)の記事も参考になりました。

※また、英文ですが[「こちら」](https://github.com/alexa/Alexa-Gadgets-Raspberry-Pi-Samples#registering-a-gadget-in-the-alexa-voice-service-developer-console)の以下の項目も参考になります（サンプルコードの提供元）
・Registering a gadget in the Alexa Voice Service Developer Console
・Installation
・Pairing your gadget to an Echo device　


# AWS Lambdaの変更

1. リージョンを「東京」に変更します（必要ないかも）
2. 関数コード（スキルコード）でindex.jsを以下のように変更します（日本語対応）

<details><summary>index.js（変更箇所の抜粋）</summary><div>
　
※index.jsでは以下の各ハンドラのコードを変更しました

 * LaunchRequestHandler：「ラズパイの情報を開いて」と聞いた際の振る舞い
 * YesIntentHandler：「IPアドレスですね？」→「はい」と答えた場合の通知をラズパイに送る
 * NoIntentHandler：「IPアドレスですね？」→「いいえ」と答えた場合の通知をラズパイに送る
 * CustomInterfaceEventHandler：ラズパイから帰ってきたデータ（IPアドレス）を処理する

```index.js（LaunchRequestHandler）
    LaunchRequestHandler: {
　　　　　　　　　　：（省略）
                if ((response.endpoints || []).length === 0) {
                    console.log('No connected gadget endpoints available');
                    response = handlerInput.responseBuilder
                        // .speak("No gadgets found. Please try again after connecting your gadget.")
                        .speak("ガジェットが見つかりません。ガジェットとの接続を確認してください。")
                        .getResponse();
                    return response;
                }

　　　　　　　　　　：（省略）

                return handlerInput.responseBuilder
                    .speak("IPアドレスの取得ですね？")
                    .withShouldEndSession(false)
                    .getResponse();
                // return handlerInput.responseBuilder
                //     .speak("Hi! I will cycle through a spectrum of colors. " +
                //         "When you press the button, I'll report back which color you pressed. Are you ready?")
                //     .withShouldEndSession(false)
                //     // Send the BlindLED directive to make the LED green for 20 seconds.
                //     .addDirective(buildBlinkLEDDirective(endpointId, ['GREEN'], 1000, 20, false))
                //     .getResponse();
            }
```

```index.js（YesIntentHandler）

    YesIntentHandler: {

　　　　　　　　　　：（省略）

            return handlerInput.responseBuilder
                // Send the BlindLEDDirective to trigger the cycling animation of the LED.
                .addDirective(buildBlinkLEDDirective(endpointId, ['RED', 'YELLOW', 'GREEN', 'CYAN', 'BLUE', 'PURPLE', 'WHITE'],
                    1000, 2, true))
                // Start a EventHandler for 10 seconds to receive only one
                // 'Custom.ColorCyclerGadget.ReportColor' event and terminate.
                // .addDirective(buildStartEventHandlerDirective(sessionAttributes.token, 10000,
                //     'Custom.ColorCyclerGadget', 'ReportColor', 'SEND_AND_TERMINATE',
                //     { 'data': "You didn't press the button. Good bye!" }))
                .addDirective(buildStartEventHandlerDirective(sessionAttributes.token, 10000,
                    'Custom.ColorCyclerGadget', 'ReportColor', 'SEND_AND_TERMINATE',
                    { 'data': "お返事がありませんでした。終了します。" }))
                .getResponse();
        }
    },
```

```index.js（NoIntentHandler）
    NoIntentHandler: {

　　　　　　　　　　：（省略）

            return handlerInput.responseBuilder
                .addDirective(buildStopLEDDirective(sessionAttributes.endpointId))
                // .speak("Alright. Good bye!")
                .speak("わかりました。終了します。")
                .withShouldEndSession(true)
                .getResponse();
        }
    },
```

```index.js（CustomInterfaceEventHandler）
    CustomInterfaceEventHandler: {

　　　　　　　　　　：（省略）

            if (namespace === 'Custom.ColorCyclerGadget' && name === 'ReportColor') {
                // On receipt of 'Custom.ColorCyclerGadget.ReportColor' event, speak the reported color
                // and end skill session.
                // return response.speak(payload.color + ' is the selected color. Thank you for playing. Good bye!')
                return response.speak('IPアドレスは' + payload.color + 'です。')
                    .withShouldEndSession(true)
                    .getResponse();
            }
            return response;
        }
    },
```

</div></details>

<details><summary>index.js（All）</summary><div>

```index.js

//
// Copyright 2019 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
// These materials are licensed under the Amazon Software License in connection with the Alexa Gadgets Program.
// The Agreement is available at https://aws.amazon.com/asl/.
// See the Agreement for the specific terms and conditions of the Agreement.
// Capitalized terms not defined in this file have the meanings given to them in the Agreement.
//
'use strict';

const Alexa = require('ask-sdk-core');
const Https = require('https');
const Uuid = require('uuid/v4');

let skill;
exports.handler = function(event, context) {

    if (!skill) {
        skill = Alexa.SkillBuilders.custom()
            .addRequestHandlers(
                handler.LaunchRequestHandler,
                handler.YesIntentHandler,
                handler.NoIntentHandler,
                handler.CustomInterfaceEventHandler,
                handler.CustomInterfaceExpirationHandler,
                handler.StopAndCancelIntentHandler,
                handler.SessionEndedRequestHandler,
                handler.DefaultHandler
            )
            .addRequestInterceptors(handler.RequestInterceptor)
            .addResponseInterceptors(handler.ResponseInterceptor)
            .addErrorHandlers(handler.ErrorHandler)
            .create();
    }
    return skill.invoke(event, context);
};

const handler = {
    LaunchRequestHandler: {
        canHandle(handlerInput) {
            let { request } = handlerInput.requestEnvelope;
            console.log("LaunchRequestHandler: checking if it can handle " + request.type);
            return request.type === 'LaunchRequest';
        },
        async handle(handlerInput) {
            console.log("== Launch Intent ==");
            console.log(JSON.stringify(handlerInput.requestEnvelope));

            let { context } = handlerInput.requestEnvelope;
            let { apiEndpoint, apiAccessToken } = context.System;
            let response;
            try {
                // Get connected gadget endpointId.
                console.log("Checking endpoint");
                response = await getConnectedEndpoints(apiEndpoint, apiAccessToken);
                console.log("v1/endpoints response: " + JSON.stringify(response));

                if ((response.endpoints || []).length === 0) {
                    console.log('No connected gadget endpoints available');
                    response = handlerInput.responseBuilder
                        // .speak("No gadgets found. Please try again after connecting your gadget.")
                        .speak("ガジェットが見つかりません。ガジェットとの接続を確認してください。")
                        .getResponse();
                    return response;
                }

                let endpointId = response.endpoints[0].endpointId;

                // Store endpointId for using it to send custom directives later.
                console.log("Received endpoints. Storing Endpoint Id: " + endpointId);
                const attributesManager = handlerInput.attributesManager;
                let sessionAttributes = attributesManager.getSessionAttributes();
                sessionAttributes.endpointId = endpointId;
                attributesManager.setSessionAttributes(sessionAttributes);

                return handlerInput.responseBuilder
                    .speak("IPアドレスの取得ですね？")
                    .withShouldEndSession(false)
                    .getResponse();
                // return handlerInput.responseBuilder
                //     .speak("Hi! I will cycle through a spectrum of colors. " +
                //         "When you press the button, I'll report back which color you pressed. Are you ready?")
                //     .withShouldEndSession(false)
                //     // Send the BlindLED directive to make the LED green for 20 seconds.
                //     .addDirective(buildBlinkLEDDirective(endpointId, ['GREEN'], 1000, 20, false))
                //     .getResponse();
            }
            catch (err) {
                console.log("An error occurred while getting endpoints", err);
                response = handlerInput.responseBuilder
                    .speak("I wasn't able to get connected endpoints. Please try again.")
                    .withShouldEndSession(true)
                    .getResponse();
                return response;
            }
        }
    },
    YesIntentHandler: {
        canHandle(handlerInput) {
            let { request } = handlerInput.requestEnvelope;
            let intentName = request.intent ? request.intent.name : '';
            console.log("YesIntentHandler: checking if it can handle " +
                request.type + " for " + intentName);
            return request.intent && request.intent.name === 'AMAZON.YesIntent';
        },
        handle(handlerInput) {
            // Retrieve the stored gadget endpointId from the SessionAttributes.
            const attributesManager = handlerInput.attributesManager;
            let sessionAttributes = attributesManager.getSessionAttributes();
            let endpointId = sessionAttributes.endpointId;

            // Create a token to be assigned to the EventHandler and store it
            // in session attributes for stopping the EventHandler later.
            sessionAttributes.token = Uuid();
            attributesManager.setSessionAttributes(sessionAttributes);

            console.log("YesIntent received. Starting game.");

            return handlerInput.responseBuilder
                // Send the BlindLEDDirective to trigger the cycling animation of the LED.
                .addDirective(buildBlinkLEDDirective(endpointId, ['RED', 'YELLOW', 'GREEN', 'CYAN', 'BLUE', 'PURPLE', 'WHITE'],
                    1000, 2, true))
                // Start a EventHandler for 10 seconds to receive only one
                // 'Custom.ColorCyclerGadget.ReportColor' event and terminate.
                // .addDirective(buildStartEventHandlerDirective(sessionAttributes.token, 10000,
                //     'Custom.ColorCyclerGadget', 'ReportColor', 'SEND_AND_TERMINATE',
                //     { 'data': "You didn't press the button. Good bye!" }))
                .addDirective(buildStartEventHandlerDirective(sessionAttributes.token, 10000,
                    'Custom.ColorCyclerGadget', 'ReportColor', 'SEND_AND_TERMINATE',
                    { 'data': "お返事がありませんでした。終了します。" }))
                .getResponse();
        }
    },
    NoIntentHandler: {
        canHandle(handlerInput) {
            let { request } = handlerInput.requestEnvelope;
            let intentName = request.intent ? request.intent.name : '';
            console.log("NoIntentHandler: checking if it can handle " +
                request.type + " for " + intentName);
            return request.intent && request.intent.name === 'AMAZON.NoIntent';
        },
        handle(handlerInput) {
            console.log("Received NoIntent..Exiting.");
            const attributesManager = handlerInput.attributesManager;
            let sessionAttributes = attributesManager.getSessionAttributes();

            // Send StopLED directive to stop LED animation and end skill session.
            return handlerInput.responseBuilder
                .addDirective(buildStopLEDDirective(sessionAttributes.endpointId))
                // .speak("Alright. Good bye!")
                .speak("わかりました。終了します。")
                .withShouldEndSession(true)
                .getResponse();
        }
    },
    CustomInterfaceEventHandler: {
        canHandle(handlerInput) {
            let { request } = handlerInput.requestEnvelope;
            console.log("CustomEventHandler: checking if it can handle " + request.type);
            return request.type === 'CustomInterfaceController.EventsReceived';
        },
        handle(handlerInput) {
            console.log("== Received Custom Event ==");

            let { request } = handlerInput.requestEnvelope;

            const attributesManager = handlerInput.attributesManager;
            let sessionAttributes = attributesManager.getSessionAttributes();

            // Validate eventHandler token
            if (sessionAttributes.token !== request.token) {
                console.log("EventHandler token doesn't match. Ignoring this event.");
                return handlerInput.responseBuilder
                    .speak("EventHandler token doesn't match. Ignoring this event.")
                    .getResponse();
            }

            let customEvent = request.events[0];
            let payload = customEvent.payload;
            let namespace = customEvent.header.namespace;
            let name = customEvent.header.name;

            let response = handlerInput.responseBuilder;

            if (namespace === 'Custom.ColorCyclerGadget' && name === 'ReportColor') {
                // On receipt of 'Custom.ColorCyclerGadget.ReportColor' event, speak the reported color
                // and end skill session.
                // return response.speak(payload.color + ' is the selected color. Thank you for playing. Good bye!')
                return response.speak('IPアドレスは' + payload.color + 'です。')
                    .withShouldEndSession(true)
                    .getResponse();
            }
            return response;
        }
    },
    CustomInterfaceExpirationHandler: {
        canHandle(handlerInput) {
            let { request } = handlerInput.requestEnvelope;
            console.log("CustomEventHandler: checking if it can handle " + request.type);
            return request.type === 'CustomInterfaceController.Expired';
        },
        handle(handlerInput) {
            console.log("== Custom Event Expiration Input ==");

            let { request } = handlerInput.requestEnvelope;

            const attributesManager = handlerInput.attributesManager;
            let sessionAttributes = attributesManager.getSessionAttributes();

            // When the EventHandler expires, send StopLED directive to stop LED animation
            // and end skill session.
            return handlerInput.responseBuilder
                .addDirective(buildStopLEDDirective(sessionAttributes.endpointId))
                .withShouldEndSession(true)
                .speak(request.expirationPayload.data)
                .getResponse();
        }
    },
    StopAndCancelIntentHandler: {
        canHandle(handlerInput) {
            const { request } = handlerInput.requestEnvelope;
            const intentName = request.intent ? request.intent.name : '';
            console.log("StopAndCancelIntentHandler: checking if it can handle " +
                request.type + " for " + intentName);
            return request.type === 'IntentRequest' &&
                (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent');
        },
        handle(handlerInput) {
            console.log("Received a Stop or a Cancel Intent..");

            let { attributesManager, responseBuilder } = handlerInput;
            let sessionAttributes = attributesManager.getSessionAttributes();

            // When the user stops the skill, stop the EventHandler,
            // send StopLED directive to stop LED animation and end skill session.
            if (sessionAttributes.token) {
                console.log("Active session detected, sending stop EventHandlerDirective.");
                responseBuilder.addDirective(buildStopEventHandlerDirective(sessionAttributes.token));
            }

            return responseBuilder.speak("Alright. see you later.")
                .addDirective(buildStopLEDDirective(sessionAttributes.endpointId))
                .withShouldEndSession(true)
                .getResponse();
        }
    },
    SessionEndedRequestHandler: {
        canHandle(handlerInput) {
            return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
        },
        handle(handlerInput) {
            console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`);
            return handlerInput.responseBuilder.getResponse();
        },
    },
    ErrorHandler: {
        canHandle(handlerInput, error) {
            let { request } = handlerInput.requestEnvelope;
            console.log("ErrorHandler: checking if it can handle " +
                request.type + ": [" + error.name + "] -> " + !!error.name);
            return !!error.name;
        },
        handle(handlerInput, error) {
            console.log("Global.ErrorHandler: error = " + error.message);

            return handlerInput.responseBuilder
                .speak("I'm sorry, something went wrong!")
                .getResponse();
        }
    },
    RequestInterceptor: {
        process(handlerInput) {
            let { attributesManager, requestEnvelope } = handlerInput;
            let sessionAttributes = attributesManager.getSessionAttributes();

            // Log the request for debugging purposes.
            console.log(`==Request==${JSON.stringify(requestEnvelope)}`);
            console.log(`==SessionAttributes==${JSON.stringify(sessionAttributes, null, 2)}`);
        }
    },
    ResponseInterceptor: {
        process(handlerInput) {

            let { attributesManager, responseBuilder } = handlerInput;
            let response = responseBuilder.getResponse();
            let sessionAttributes = attributesManager.getSessionAttributes();

            // Log the response for debugging purposes.
            console.log(`==Response==${JSON.stringify(response)}`);
            console.log(`==SessionAttributes==${JSON.stringify(sessionAttributes, null, 2)}`);
        }
    },
    DefaultHandler: {
        canHandle(handlerInput) {
            let { request } = handlerInput.requestEnvelope;
            let intentName = request.intent ? request.intent.name : '';
            console.log("DefaultHandler: checking if it can handle " +
                request.type + " for " + intentName);
            return true;
        },
        handle(handlerInput) {
            console.log("Unsupported Intent receive..Exiting.");
            return handlerInput.responseBuilder
                .speak("Unsupported Intent received. Exiting.")
                .getResponse();
        }
    }
};

function getConnectedEndpoints(apiEndpoint, apiAccessToken) {
    apiEndpoint = (apiEndpoint || '').replace('https://', '');

    return new Promise(((resolve, reject) => {
        var options = {
            host: apiEndpoint,
            path: '/v1/endpoints',
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + apiAccessToken
            }
        };

        const request = Https.request(options, (response) => {
            response.setEncoding('utf8');
            let returnData = '';

            response.on('data', (chunk) => {
                returnData += chunk;
            });

            response.on('end', () => {
                resolve(JSON.parse(returnData));
            });

            response.on('error', (error) => {
                reject(error);
            });
        });
        request.end();
    }));
}

function buildBlinkLEDDirective(endpointId, colors_list, intervalMs, iterations, startGame) {
    return {
        type: 'CustomInterfaceController.SendDirective',
        header: {
            name: 'BlinkLED',
            namespace: 'Custom.ColorCyclerGadget'
        },
        endpoint: {
            endpointId: endpointId
        },
        payload: {
            colors_list: colors_list,
            intervalMs: intervalMs,
            iterations: iterations,
            startGame: startGame
        }
    };
}

function buildStopLEDDirective(endpointId) {
    return {
        type: 'CustomInterfaceController.SendDirective',
        header: {
            name: 'StopLED',
            namespace: 'Custom.ColorCyclerGadget'
        },
        endpoint: {
            endpointId: endpointId
        },
        payload: {}
    };
}

function buildStartEventHandlerDirective(token, durationMs, namespace, name, filterMatchAction, expirationPayload) {
    return {
        type: "CustomInterfaceController.StartEventHandler",
        token: token,
        eventFilter: {
            filterExpression: {
                'and': [
                    { '==': [{ 'var': 'header.namespace' }, namespace] },
                    { '==': [{ 'var': 'header.name' }, name] }
                ]
            },
            filterMatchAction: filterMatchAction
        },
        expiration: {
            durationInMilliseconds: durationMs,
            expirationPayload: expirationPayload
        }
    };
}

function buildStopEventHandlerDirective(token) {
    return {
        type: "CustomInterfaceController.StopEventHandler",
        token: token
    };
}

```

</div></details>

# Alexa Developer Consoleの変更

[Alexa Developer Console](https://developer.amazon.com/alexa/console/ask)で先ほど作成したスキルを開いて以下の変更を行います。

### 言語の変更

「Language Settings」で「Japanese」を追加してそちらに変更します（下図）
![LanguageSettings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155449/69c8e116-ea3e-2757-ba5c-adff63eaa8a6.png)


### 呼び出し名の変更
![呼び出し名の変更.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155449/7eb92676-bbef-6b12-37e9-25de6ddfede7.png)

### AMAZON.YesIntentとAMAZON.NoIntentの追加

以下の画像のようにAMAZON.YesIntentとAMAZON.NoIntentを追加します。
![インテントの追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155449/3d7c1b11-eb83-ed2c-6209-46726b9191d7.png)
 
※それぞれのサンプル発話は以下の通りに登録します
AMAZON.YesIntent→「はい」
AMAZON.NoIntent→「いいえ」
![インテントYESNO.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155449/9689c459-aafe-5096-fecc-0a7c697eb0ac.png)
　
# ラズパイ内のサンプルアプリ(Python)の変更

１．（インストールしていない場合、）netifacesをインストールします

```
$ sudo pip3 install netifaces
```

２．「Alexa-Gadgets-Raspberry-Pi-Samples/src/examples/colorcycler/」内のcolor_cycler.pyを以下のように変更します

<details><summary>color_cycler.py</summary><div>
　
color_cyclerをベースにして必要な部分だけ残してIPアドレスを返す処理を加えました。また、Alexaからの通知に応じてLED(21番ピン)をON/OFFさせています。
※index.jsの各ハンドラとcolor_cycler.pyのメソッドは以下のように対応しています

 * LaunchRequestHandler→　\_\_init\_\_
 * YesIntentHandler→　on_custom_colorcyclergadget_blinkled
 * NoIntentHandler→　on_custom_colorcyclergadget_stopled

```color_cycler.py
import json
import logging
import sys
import threading
import time

from colorzero import Color
from agt import AlexaGadget

from gpiozero import LED
import netifaces

logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logger = logging.getLogger(__name__)

GPIO_PIN = 21
LED = LED(GPIO_PIN)

class ColorCyclerGadget(AlexaGadget):

    def __init__(self):
        super().__init__()
        LED.off()

    def on_custom_colorcyclergadget_blinkled(self, directive):
        # Send custom event to skill
        logger.info('Send custom event to skill')
        payload = {'color': netifaces.ifaddresses('wlan0')[netifaces.AF_INET][0]['addr']}
        self.send_custom_event(
            'Custom.ColorCyclerGadget', 'ReportColor', payload)
        LED.on()

    def on_custom_colorcyclergadget_stopled(self, directive):
        LED.off()

if __name__ == '__main__':
    try:
        ColorCyclerGadget().main()
    finally:
        LED.close()

```

</div></details>

# Pythonアプリの実行

pythonアプリを実行すると、以下のようにラズパイとecho dotが繋がりますので、その状態でecho dotに向かって「アレクサ、ラズパイの情報を開いて」と話しかければＯＫです。（「アレクサ、ラズパイの情報」でも良いようです）

```
$ python3 color_cycler.py 
INFO:agt.alexa_gadget:Attempting to reconnect to Echo device with address: XX:XX:XX:XX:XX:XX
INFO:agt.alexa_gadget:Connected to Echo device with address: XX:XX:XX:XX:XX:XX
```

※ラズパイのBluetoothのONをお忘れなく

# 雑感
今回は自作のVUI(Voice User Interface)というものをちょこっと体験してみましたが、なかなか便利な感じです。

そして、今回はIPアドレスの取得でしたが、同様にラズパイを介してクラウドやセンサから温度などのデータを取得すると、さらに便利に使えそうです。

また、Alexa Gadgets Toolkitはラズパイ3B+が[推奨](https://github.com/alexa/Alexa-Gadgets-Raspberry-Pi-Samples#prerequisites) されていますので、本記事のように3Bを使用していると何か不都合があるかもしれませんので、ご注意を・・・。

見ていただいてありがとうございました。
тнайк чoμ_〆(・ω・｡)			
			

# 参考記事
 + Alexa Gadgets Toolkitとは(Amazon公式)
   + https://developer.amazon.com/ja/docs/alexa-gadgets-toolkit/understand-alexa-gadgets-toolkit.html
 + Alexaのカスタムスキルがガジェットと連携！ 〜Alexa Gadgets Toolkit の Custom Interfaces を試す〜
  + https://qiita.com/youtoy/items/472da6c009a5ee3407ea
 + Alexa-Gadgets-Raspberry-Pi-Samples
  + https://github.com/alexa/Alexa-Gadgets-Raspberry-Pi-Samples
 + カスタムインターフェースの詳細(Amazon Alexa公式)
  + https://developer.amazon.com/ja/docs/alexa-gadgets-toolkit/custom-interface.html#overview
 + ラズパイとAmazonアレクサを連携してAlexa Gadgetを使う
  + https://raspida.com/connect-rpi3bp-alexagadget
 + RaspberryPiの起動時にslackにIPアドレスを投稿する
  + https://tmegane.hatenablog.com/entry/2018/10/19/104708



# 更新履歴
* 2019-11-21：新規作成 
* 2019-11-22：以下の項目を更新
  + 図：本環境の概要
  + 使用したもの
  + Alexa Developer Consoleの変更　→「言語の変更」を追加
  + 雑感