Ameba Arduino: [RTL8195] Amazon Alexa

使用Amazon Alexa控制Ameba

Alexa是Amazon底下的语音服务, 它可以连结至Amazon其它服务, 完成许多功能。著名的应用像是Amazon Echo, 功能定为在语音管家, 使用者可以对Amazon Echo说话, Echo解析这段语音, 做出适合的回应。
这个范例里会介绍如何使用Alexa控制Ameba上的LED, 涵盖的服务包括Amazon Alexa, AWS Lambda, AWS IoT, AWS IAM。

材料准备

  • Ameba x 1
  • LED灯泡 x 1

范例说明

底下是范例里的使用情境
(1) 使用者对支援Amazon Alexa服务的装置说 “Turn on the light”, Alexa根据使用者提供的schema与sample utterances file,解析之后产生intent json
(2) Alexa提交intent json至AWS Lambda, Lambda根据intent以及内容, 更新AWS IoT Shadow service
(3) AWS IoT Shadow service收到Lambda的讯息, 更新shadow state
(3.1) 此时如果Ameba在线上并且subscribe对应的shadow service, 就点亮LED
(3.2) Lambda产生回应至Alexa的文字与语音讯息, 并递交给Alexa
(4) 使用者听到成功点亮LED的语音讯息
1
其中Lambda取用其他服务时, 牵涉到权限的问题, 这部份会使用到AWS IAM的服务
底下各个章节分别介绍各个服务与范例的设定

Amazon Alexa – short introduction

Alexa Skills Kit (ASK) 是voice-driven的服务, 它可以连结至云端服务, 让使用者可以使用语音完成云端服务, 并且得到语音的回应, 其中语音的解析让使用者省去不少语音辨识的麻烦, 让使用者可以专心于设计互动模型与云端服务上。

Amazon Alexa - Custom Skills ag. Smart Home Skills

Alexa的服务可以分成两类
(1) Custom Skills: 使用者可以根据自己的需求设计互动模型, 包括对话流程, 解析关键字, 并发出intent
(2) Smart Home Skills: 使用者使用Alexa已经建制好的smart home的模型, 并且发出intent
这两类其实概念上很类似,Smart Home Skills省去一些麻烦,但考量到设计上的弹性, 这个范例里我们使用Custom Skills。
Amazon官方文件里有对这两类的比较有更详细的说明:
https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/understanding-the-different-types-of-skills

Amazon Alexa - Create Skill

首先我们先登入Alexa的开发网页, 点选Sign In, 填入的帐号可以与Amazon AWS的帐号不同
https://developer.amazon.com/alexa
2
接着点选Alex Skill kit > Get Started > Alex Skills Kit (Make Alexa smarter with our toolkit)
3

带到Alexa Skill Kit的页面后,点击”Start a Skill”
4

于页面下方点击”Create Skill”
5

Amazon Alexa - Create Flow

点选 “Start a Skill” 之后, 开始进入设定的流程,在skill name栏位里输入” Control Light of Ameba”, 这个地方填的是要显示给一般使用者的名称,接着点击“Next”
6

选择“Custom”,接着点击“Create Skill”
6

进入下个页面后,于左侧选单Slot Type点击“add”
6

进入Add Slot Type页面后,于Create custom slot type栏位输入“LIGHT_STATE”,并点击“Create custom slot type”
进入下个页面后,于左侧选单Slot Type点击“add”
6

进入新增Slot Types的页面后,分别输入“on”及“off”于Slot Values栏位,并点击“+”
6

回到先前的Skill home page,在页面右边Skill builder checklist里,点击”Invocation Name”
6

在” Skill Invocation Name”栏位输入”ameba controller”, 这里填的是让Alexa识别要启动session的名称。一般来说, Alexa启动session的方式有两种, 第一种是讲出完整的内容, 让Alexa分析互动模型来决定要启动哪个session, 第二种是使用者只讲Invocation Name, 让Alexa启动特定的session。这里我们填入 “Ameba Controller”

Invocation Name的取名有一些限制, 要避开Alexa设定的关键字, 以及会造成误判语意的名称, 详细的限制说明可以参考这里:
https://developer.amazon.com/zh/docs/custom-skills/choose-the-invocation-name-for-a-custom-skill.html

6

并点击“Save model”

6

回到Skill home page,在页面右边Skill builder checklist里,点击“Intents, Samples, and Slots”

6

在Add intent栏位中,在Create custom intent栏位输入ControlLight,并点击“Create custom intent”

6

接着我们填写 Sample Utterances, 这里要填的是当使用者说了什么句子时, 可以触发哪些intent, 我们填入以下的值

ControlLight Turn {LightState} the light
ControlLight Turn the light {LightState}

它的格式里, 前面带的是intent名称, 接着是空白或tab, 后面跟着是使用者说的句子, 以第一行来说, 代表当使用者说了“Turn on the light”会触发ControlLight intent ,最后选Slot Type下方选择LIGHT_STATE,并点击“Save Model”

6

回到Skill Home Page,下方EndPoint的选项里需要填写与Alexa衔接的Endpoint。当使用者触发Alexa的intent之后, Alexa会将这个intent传递给其它service处理, 使用者可以自己架构这样的server, 或是使用Amazon AWS Lambda, 这里我们将暂停设定Alexa, 并且设定AWS Lambda, 稍后再将这部份完成

6

AWS Lambda – short introduction

AWS Lambda是Amazon提供的计算服务, 它目前提供的程式语言有Node.js, Python, 以及java, 使用者可以撰写程式码, 并且使用AWS其它服务 (Ex. AWS IoT, logger)。 Lambda让使用者设定计算所需要的资源, 像是记忆体, 运算时间等等,Lambda会处理运算的部份。

AWS Lambda – Create and select blueprint

我们在浏览器开新的分页, 并且进入AWS Lambda的首页 https://aws.amazon.com/lambda/
点选右边 “Sign in to the Console”
15

登入之后, 会进到AWS的服务列表, 因为写这篇文章的时候, Alexa与Lambda衔接的服务只支援us-east-1的区域, 所以我们先切换region, 点选右上角设定region, 选择“ US East (N. Virginia)

16

然后在左边Compute相关的服务里, 点选Lambda

17

点选 “Create a Function”

17

Lambda预先设计了一些使用情境, 这些情境提供了一些sample code与default setting。要使用Alexa与Lambda衔接, 我们在Filter的地方填入Alexa

19

填完之后会筛选出与Alexa相关的blueprint, 其中“alexa-skills-kit-color-expert”与“alexa-skills-kit-color-export-python”是相似的blueprint, 差别只在于“alexa-skills -kit-color-expert”使用的程式语言是JavaScript, 而“alexa-skills-kit-color-export-python”使用python, 这里我们选择“alexa-skills-kit-color-expert”,并点击“configure ”

AWS Lambda – Configure function

进入alexa-skills-kit-color-expert设定页,在name栏位填入“ControlLight”,用来识别这个Lambda function,且选择“Create a custom role”, “Role”的设定关系到Lambda的权限,为了避免Lambda function使用了不该使用的服务, 我们可以设定Lambda function的权限, 这部份的服务来自于AWS IAM, 不过我们可以在这里直接设定
20

AWS IAM – Create role from AWS Lambda

在Lambda带出的AWS IAM的设定页面里, 我们设定 “Role Name”为 “control_light”,并点击 “Edit”编辑Policy Document”

25

然后我们可以编辑这个role的权限, 可以看到预设值里, 这个Role的权限只有log的权限, 因为我们需要使用AWS IoT的权限, 所以我们修改它如下

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:*"
      ],
      "Resource": "arn:aws:iot:*:*:*"
    }
  ]
}

接着点选 “Allow”
接着页面会关闭, 我们会跳回前一个Lambda设定的页面

AWS Lambda – Configure function (cont.)

设定新的Role并且从AWS IAM返回之后, 可以看到 “Role”以及 “Existing Role”的栏位已经有值

30

接着接到页面下方在kill ID verification栏位选择“Enable”,另外还有Skill ID的部份,我们在下一章节介绍如何拿到Skill ID

AWS Lambda – Get the Skill ID

接着我们要回到Alexa Skill的首页,我们可以看到在Skill Name栏位的下方,已出现一组字码,这即是我们需要的SKILL ID,点击他将会自动copy,并且回到上一步的SKILL ID栏位填入SKILL ID

30

30

Kill ID设定好之后,接着要将Function一起设定完成,将页面拉至最下方,点击“Create function”

30

此时会带入Function Home Page

30

请注意,Function Home Page的右上角有一组字串“ARN - arn:aws:lambda:us-east-1:553661462376:function:ControlLight”,就是这个Lambda function的end point, 这个会用来填入Alexa的设定页面

在” Function code”栏位下依序选择“Edit code inline”, “Node.js 4.3” 及“index.handler”

30

到页面下方处,输入“3” sec在Timeout栏位,并在Network栏位选择“No VPC”,接着点击右上角的Save键,这里我们暂停AWS Lambda的设定,我们将会再回来编辑程式码的部份

30

AWS Lambda – Configure test events

30

AWS IoT – Create thing

请参考之前的文章将Ameba与AWS IoT Shadow的设定完成:
https://www.amebaiot.com/ameba-arduino-amazon-aws-iot/
example的部份, 我们使用 "File" -> "Examples" -> "AmebaMQTTClient" -> "amazon_awsiot_with_ack"
因为Alexa的限制需要将region改成us-east-1, 这部份需要特别注意

34

完成之后, 我们点选ameba的thing, 让右边的资讯栏出现, 其中REST API endpoint里, 这个字串“a2zweh2b7yb784.iot.us-east-1.amazonaws.com”是AWS IoT提供给其它服务使用的endpoint, 这个endpoint我们会拿来填入Lambda所需的资讯里

35

AWS Lambda – coding

让我们再回到Lambda web page,左边选择Functions,并且会看到刚刚产生的ControlLight funcion,点击ControlLight之后,带入Configure页面

32

32

将页面拉至下方程式码编辑处

33

然后我们根据原本的程式码修改如下:

/**
 * This sample demonstrates a simple skill built with the Amazon Alexa Skills Kit.
 * The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well as
 * testing instructions are located at http://amzn.to/1LzFrj6
 *
 * For additional samples, visit the Alexa Skills Kit Getting Started guide at
 * http://amzn.to/1LGWsLG
 */

var AWS = require('aws-sdk');
AWS.config.region = "us-east-1";
var iotData = new AWS.IotData({endpoint: "a2zweh2b7yb784.iot.us-east-1.amazonaws.com"});

// Route the incoming request based on type (LaunchRequest, IntentRequest, // etc.) The JSON body of the request is provided in the event parameter.
exports.handler = function (event, context) {
    try {
        console.log("event.session.application.applicationId=" + event.session.application.applicationId);

        /**
         * Uncomment this if statement and populate with your skill's application ID to
         * prevent someone else from configuring a skill that sends requests to this function.
         */
        /*
        if (event.session.application.applicationId !== "amzn1.echo-sdk-ams.app.[unique-value-here]") {
             context.fail("Invalid Application ID");
        }
        */

        if (event.session.new) {
            onSessionStarted({requestId: event.request.requestId}, event.session);
        }

        if (event.request.type === "LaunchRequest") {
            onLaunch(event.request,
                event.session,
                function callback(sessionAttributes, speechletResponse) {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === "IntentRequest") {
            onIntent(event.request,
                event.session,
                function callback(sessionAttributes, speechletResponse) {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === "SessionEndedRequest") {
            onSessionEnded(event.request, event.session);
            context.succeed();
        }
    } catch (e) {
        context.fail("Exception: " + e);
    }
};

/**
 * Called when the session starts.
 */
function onSessionStarted(sessionStartedRequest, session) {
    console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId +
        ", sessionId=" + session.sessionId);
}

/**
 * Called when the user launches the skill without specifying what they want.
 */
function onLaunch(launchRequest, session, callback) {
    console.log("onLaunch requestId=" + launchRequest.requestId +
        ", sessionId=" + session.sessionId);

    // Dispatch to your skill's launch.
    getWelcomeResponse(callback);
}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest, session, callback) {
    console.log("onIntent requestId=" + intentRequest.requestId +
        ", sessionId=" + session.sessionId);

    var intent = intentRequest.intent,
        intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if ("ControlLight" === intentName) {
        setLightInSession(intent, session, callback);
    } else if ("AMAZON.HelpIntent" === intentName) {
        getWelcomeResponse(callback);
    } else if ("AMAZON.StopIntent" === intentName || "AMAZON.CancelIntent" === intentName) {
        handleSessionEndRequest(callback);
    } else {
        throw "Invalid intent";
    }
}

/**
 * Called when the user ends the session.
 * Is not called when the skill returns shouldEndSession=true.
 */
function onSessionEnded(sessionEndedRequest, session) {
    console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId +
        ", sessionId=" + session.sessionId);
    // Add cleanup logic here
}

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

function getWelcomeResponse(callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    var sessionAttributes = {};
    var cardTitle = "Welcome";
    var speechOutput = "Welcome to the Ameba Controller example. " +
        "Please tell me next action by saying, turn on the light";

    // If the user either does not reply to the welcome message or says something that is not
    // understood, they will be prompted again with this text.
    var repromptText = "Please tell me next action by saying, turn on the light";
    var shouldEndSession = false;

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    var cardTitle = "Session Ended";
    var speechOutput = "Thank you for trying the Ameba Controller example. Have a nice day!";
    // Setting this to true ends the session and exits the skill.
    var shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

/**
 * Sets the led in the session and prepares the speech to reply to the user.
 */
function setLightInSession(intent, session, callback) {
    var cardTitle = intent.name;
    var lightStateRequest = intent.slots.LightState;
    var repromptText = "";
    var sessionAttributes = {};
    var shouldEndSession = true;
    var speechOutput = "";

    if (lightStateRequest) {
        var lightState = lightStateRequest.value;
        var paramsUpdate;

        if (lightState === "on") {
            paramsUpdate = {
                "thingName" : "ameba",
                "payload" : '{"state": {"desired": {"led":1}}}'
            };
        } else {
            paramsUpdate = {
                "thingName" : "ameba",
                "payload" : '{"state": {"desired": {"led":0}}}'
            };
        }

		//Update Device Shadow
		iotData.updateThingShadow(paramsUpdate, function(err, data) {
			if (err){
				console.log(err, err.stack);

				speechOutput = "fail to update thing shadow";
				repromptText = "fail to update thing shadow";
				callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
			}
			else {
				console.log(data);

        		sessionAttributes = createLightStateAttributes(lightState);
                speechOutput = "I now know you want to turn " + lightState + " the light";
                repromptText = "I now know you want to turn " + lightState + " the light";
                callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
			}	
		});
    } else {
        speechOutput = "Please try again";
        repromptText = "Please try again";
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    }
}

function createLightStateAttributes(lightState) {
    return {
        lightState: lightState
    };
}

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

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: "PlainText",
            text: output
        },
        card: {
            type: "Simple",
            title: "SessionSpeechlet - " + title,
            content: "SessionSpeechlet - " + output
        },
        reprompt: {
            outputSpeech: {
                type: "PlainText",
                text: repromptText
            }
        },
        shouldEndSession: shouldEndSession
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: "1.0",
        sessionAttributes: sessionAttributes,
        response: speechletResponse
    };
}

一些需要注意的修改如下:
在这边, region要填入正确的region, 在endpoint的地方需要填入AWS IoT的endpoint, 也就是在thing 的资讯栏里, REST API endpoint的资讯, 请根据你创建的thing填入对应的endpoint

30

在onIntent函式里, 会比对intent的名字, 这边我们会比对我们设定的intent名字 “ControlLight”, 并且呼叫 setLightInSession的处理函式

30

在setLightInSession里面, 我们可以取得intent的slots资讯, 这边我们要取得的slot是 “LightState”

30

接着我们判断这个slot的内容, 并准备要上传至AWS IoT thing shadow的内容, 其中 “thingName”是AWS IoT我们设定好的thing name, 而payload是thing shadow的格式

30

设定好内容之后, 我们呼叫 updateThingShadow并上传至AWS IoT thing shadow

30

成功之后, 会呼叫console.log纪录内容在log里, 接着设定让Alexa回应的语音与文字内容

30

确认程式码之后, 点选File->Save

30

然后在页面右上方选择“Configure test events”

30

接着会跳出一视窗

30

这部份在于设定测试用的event, 这个代表我们可以模拟从Alexa来的event, 以及该event携带的资讯, 我们将内容填入如下, 其中跟这个范例有关的修改有intent name, slot name & value
将下列程式码贴至程式码编辑处,并在Event Name处填入”light”,接着点击”create”

{
  "session": {
    "new": false,
    "sessionId": "session1234",
    "attributes": {},
    "user": {
      "userId": null
    },
    "application": {
      "applicationId": "amzn1.echo-sdk-ams.app.[unique-value-here]"
    }
  },
  "version": "1.0",
  "request": {
    "intent": {
      "slots": {
        "LightState": {
          "name": "LightState",
          "value": "on"
        }
      },
      "name": "ControlLight"
    },
    "type": "IntentRequest",
    "requestId": "request5678"
  }
}

回到上一页后,在上方处选择刚建立的light event,并点击”Test”

30

下方会出现执行的结果与log, 在Execution result里, 会看到response的outputSpeech里讯息为 “I now know you want to turn on the light”, 这段文字将预期会让Alexa以语音方式回应。下方则是log, 如果写程式遇到麻烦需要加log, 可以在这边看到执行期间的log

30

切到AWS IoT的页面, 点选thing ameba, 在右边的资讯栏也会看到 “Last update” 会有一笔最新的更新, 代表AWS Lambda到AWS IoT这段功能已经成功

30

Amazon Alexa – EndPoint Configuration

回到Amazon Alexa Skill的主页,这次我们要来设定EndPoint,点击右下方4.Endpoint

50

接着进入编辑Endpoint的页面,选择”AWS Lambda ARN”为Service Endpoint Type,并且将刚刚拿到的ARN 字串,“arn:aws:lambda:us-east-1:553661462376:function:ControlLight”,贴至Default region栏位,请根据你创建的Lambda function的endpoint填入对应的值, (请注意, 这个endpoint并不是AWS IoT里thing的REST API endpoint, 而是Lumbda function右上角的ARN的内容) ,并点击“Save Endpoint”

50

Amazon Alexa – Test

在Test页面, 我们可以做一些基本测试

51

在左侧输入“ameba controller”来启动这个skill,我们可以测试Alexa收到的语音资料里, 它会如何发音

它使用SSML tags让Alexa发出特定的语音像是拼出hello, 点选播放键就可以听看看语音的结果

这边的文字也就是Lambda回传的outputSpeech内容, 所以我们可以在Lambda的回传语音讯息里有更多弹性

52

我们可以填入使用者发出的语音讯息, 并且让Alexa假装听到这个讯息做对应的处理,接着输入“Turn on the light”控制灯光的开启

53

这段语音讯息会被Alexa处理, 送至Lambda function, Lambda function处理完之后再回传结果, 我们点选“Voice & Tone”和”Play”可以聆听这段结果

53

同时我们可以切换浏览器页签至AWS IoT确定thing ameba有收到这份更新

到这个阶段, 我们已经将整个功能都做完了。由于这个skill只是测试用途, 我们并不会接着做上架的设定, 使用者如果有兴趣可以接着做 “Publishing Information” 与 “Privacy & Compliance”

但是在 “Test”阶段, 使用者已经可以使用自己创建的Skill, 我们接着看该如何实际测试

测试 - 支援Alexa的装置

首先我们需要支援Alexa的装置, Amazon有推出支援Alexa的装置, 你可以在这边找到相关的讯息:
https://www.amazon.com/Amazon-Echo-Bluetooth-Speaker-with-WiFi-Alexa/dp/B00X4WHP5E
或者你可以使用支援Alexa的手机应用程式, 这里我们使用IOS的应用程式:
https://itunes.apple.com/us/app/lexi-for-alexa-voice-services/id1092933088
一般来说这类app需要你登入amazon的帐户

测试 - 管理Amazon Alexa

在实际使用支援Alexa的装置时, 我们可以管理我们可以使用哪些Skill, 比如说我们可以订阅披萨公司推出的skill, 并用该skill订购披萨
你可以登入网页版的管理页面:
http://alexa.amazon.com/spa/index.html#cards
或是使用手机应用程式, 但要注意目前app只开放美国地区下载
https://www.amazon.com/gp/help/customer/display.html?nodeId=201602060
在首页里, 会出现上一次Alexa听到的讯息, 这可以帮助你厘清是程式写错或者是Alexa听错
54

测试 - Demo

实际在测试时, 因为turn on与turn off的语句与Amazon Smart home kit的关联性太高, 开关灯的其它的字眼也容易触发Smart home kit, 造成我们开发的skill无法被触发, 所以我们分两段呼叫我们的skill: 底下是影片事件流程
1. Us​​er: “Alexa, ask Ameba Controller”
这里的“Alexa, ask…”会让Alexa尝试找寻Invocation Name, 我们在“Amazon Alexa – Skill Information” 这小节里将Invocation Name设定成“Ameba Controller”, 所以当我们这样说, Alexa会开启Ameba Controller的session
2. Alexa: "Welcome to the Ameba Controller example. Please tell me next action by saying, turn on the light"
这段话出自于Lambda function里的 getWelcomeResponse(), 代表Lambda function 有收到IntentRequest的命令
3. User: “Turn on the light”
此时会等一阵子, Lambda会尝试将开灯的命令传至AWS IoT thing shadow
4. Alexa: “I now know you want to turn on the light”
此时灯亮, Lambda function在上传命令至AWS IoT thing shadow之后会关闭session, 整个流程就完成了
关灯的流程是差不多的, 就不赘述了