LoginSignup
1
2

More than 3 years have passed since last update.

Alexaでラズパイ3の情報(IPアドレス)を取得する ※手抜き版

Last updated at Posted at 2019-11-21

はじめに

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

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

図:本環境の概要

概要図.png

使用したもの

  • Echo Dot 第3世代
  • ラズパイ3B (OS: Raspbian Stretch 2019-04-08)
     →「Raspberry Pi Zero W」でも良さそうです。(参考
  • Alexa Gadgets Toolkit
  • Alexaスキル
  • AWS Lambda

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

「こちら」の「事前準備2」、「事前準備3」、「カスタムスキルの作成」を見てそのまま実施しました。※「Color Cycler Gadget」を使用します

「こちら」の記事も参考になりました。

※また、英文ですが「こちら」の以下の項目も参考になります(サンプルコードの提供元)
・Registering a gadget in the Alexa Voice Service Developer Console
・Installation
・Pairing your gadget to an Echo device 

AWS Lambdaの変更

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

index.js(変更箇所の抜粋)

 
※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;
        }
    },

index.js(All)
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
    };
}

Alexa Developer Consoleの変更

Alexa Developer Consoleで先ほど作成したスキルを開いて以下の変更を行います。

言語の変更

「Language Settings」で「Japanese」を追加してそちらに変更します(下図)
LanguageSettings.png

呼び出し名の変更

呼び出し名の変更.png

AMAZON.YesIntentとAMAZON.NoIntentの追加

以下の画像のようにAMAZON.YesIntentとAMAZON.NoIntentを追加します。
インテントの追加.png

※それぞれのサンプル発話は以下の通りに登録します
AMAZON.YesIntent→「はい」
AMAZON.NoIntent→「いいえ」
インテントYESNO.png
 

ラズパイ内のサンプルアプリ(Python)の変更

1.(インストールしていない場合、)netifacesをインストールします

$ sudo pip3 install netifaces

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

color_cycler.py

 
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()

Pythonアプリの実行

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

$ 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+が推奨 されていますので、本記事のように3Bを使用していると何か不都合があるかもしれませんので、ご注意を・・・。

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

参考記事

更新履歴

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2