はじめに
以下のようにアレクサを介してラズパイ3からIPアドレスを取得してみました。(※2019/11/21現在で有効な方法です)
「アレクサ、ラズパイの情報を開いて」(私)
↓
「IPアドレスですね?」(アレクサ)
↓
「はい」(私)
↓
「IPアドレスはxxx.xxx.xxx.xxxです」(アレクサ)
※この手順はサンプルコードに必要最低限の変更を加えた「手抜き版」です。メソッド名やメンバ名等はサンプルのままです。違和感がぱないです。そこはどうぞ良きに計らってください・・・。
図:本環境の概要
使用したもの
- 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の変更
- リージョンを「東京」に変更します(必要ないかも)
- 関数コード(スキルコード)でindex.jsを以下のように変更します(日本語対応)
index.js(変更箇所の抜粋)
- LaunchRequestHandler:「ラズパイの情報を開いて」と聞いた際の振る舞い
- YesIntentHandler:「IPアドレスですね?」→「はい」と答えた場合の通知をラズパイに送る
- NoIntentHandler:「IPアドレスですね?」→「いいえ」と答えた場合の通知をラズパイに送る
- CustomInterfaceEventHandler:ラズパイから帰ってきたデータ(IPアドレス)を処理する
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();
}
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();
}
},
NoIntentHandler: {
:(省略)
return handlerInput.responseBuilder
.addDirective(buildStopLEDDirective(sessionAttributes.endpointId))
// .speak("Alright. Good bye!")
.speak("わかりました。終了します。")
.withShouldEndSession(true)
.getResponse();
}
},
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)
//
// 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」を追加してそちらに変更します(下図)
呼び出し名の変更
AMAZON.YesIntentとAMAZON.NoIntentの追加
以下の画像のようにAMAZON.YesIntentとAMAZON.NoIntentを追加します。
※それぞれのサンプル発話は以下の通りに登録します
AMAZON.YesIntent→「はい」
AMAZON.NoIntent→「いいえ」
ラズパイ内のサンプルアプリ(Python)の変更
1.(インストールしていない場合、)netifacesをインストールします
$ sudo pip3 install netifaces
2.「Alexa-Gadgets-Raspberry-Pi-Samples/src/examples/colorcycler/」内のcolor_cycler.pyを以下のように変更します
color_cycler.py
- LaunchRequestHandler→ __init__
- YesIntentHandler→ on_custom_colorcyclergadget_blinkled
- NoIntentHandler→ on_custom_colorcyclergadget_stopled
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μ_〆(・ω・。)
参考記事
- Alexa Gadgets Toolkitとは(Amazon公式)
- 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の変更 →「言語の変更」を追加
- 雑感