透過網頁瀏覽器對ThingSpeak
IoT資料庫抓值非常簡單, 相信大家都會, 但是透過Alexa語音助理對ThingSpeak資料庫要資料就比較新鮮了! 如題, 底層是透過REST API方法, 國外許多腦筋動得快的年輕人, 他們立刻舉一反三開始透過Wikipedia或Google等REST API讓Alexa幫他們查詢/回報資料甚至自架Web Service API讓Alexa幫他們「讀」書呢. 筆者心想, 再不跟著「Me to …」可能都已跟不上時代潮流, 更甭想談創新了!
基本原理
我們於「以ESP8266存取IoT數據資料庫」中有討論到將DHT11溫/濕度測量值上傳到ThingSpeak IoT資料庫(亦就是拿ThingSpeak當我們的MQTT server),
也列舉幾種透過瀏覽器URL或以Javascript發出HTTPS/GET請求方式抓值. 其實在Alexa Skill Kit底層的方法/實作原理也是相同, 而本案例將試範如何在AWS/Lambda程式發出HTTPS/GET請求並將最新的溫/濕度測量值透過Alexa語音助理(Echo Dot)「說出來」. 整個概念如圖所示, 其中Echo設備(連接WAN)並不侷限在自己家裡.
程式架構
左半部則是新的Alexa Skill Kit (ASK) 開發者平台, 我們將在此為Alexa定義新的技能. 當使用者「調用(Invoke)」自己寫的程式名, 本例為「information」, 例如語音指令: 「Alexa, ask information, …」. 緊接著使用者的「命令語彙(utterance)」符合所預先例舉的樣本而觸發使用者自訂的「意圖(Intent)」, 本例為「GetTemperatureIntent」, 並以JSON方式要求AWS/Lambda提供底層的服務(Intent
Handler).
增加Alexa對ThinkSpeak發出HTTPS/GET請求的Skill
Step1: Skill Information
在ASK網頁新增一個叫做「information」的Skill, Skill名稱目前對初學者而言不重要, 取一個好記的名稱即可. 只有將來我們功力進步想發佈到 public domain時可能會影響搜尋的順利, 因為你將發現Alexa Skill Store也是有許多不是我們要的App(取菜市場名稱可能很難找到我們自己開發的).
Step2: Interaction Mode
ASK/Intent Schema
請參閱程式架構圖, 我們想為Alexa添增/學會對ThingSpeak資料庫查詢資料的新技能. 意圖模式(Intent Schema)是以JSON的格式來定義. 如下, 我們為自己的Skill添增一個稱為「GetTemperatureIntent」的意圖(Intent). 其中我們還引用了一個預設的「AMAZON.HelpIntent」, 目的是當我們調用自己的App卻沒有給任何語音指令時將觸發該Intent, 而其對應的處理函式為「getWelcomeResponse」.
{
"intents": [
{
"intent": "GetTemperatureIntent"
},
{
"intent": "AMAZON.HelpIntent"
}
]
}
|
ASK/Sample utterances
為了讓Alexa能成功辨識使用者語音指令, 並觸發我們所定義的「GetTemperatureIntent」, 因此我們須先預想幾種可能的使用者語音輸入樣本以成功觸發該Intent, 例如:
GetTemperatureIntent get info
GetTemperatureIntent get
information
GetTemperatureIntent get the
temprature
GetTemperatureIntent check
thingspeak
GetTemperatureIntent query
thingspeak
GetTemperatureIntent what is
the temperature
GetTemperatureIntent give me
the temperature
|
Step3: 撰寫Lambda程式
這個步驟須將焦點暫時從ASK網頁移到AWS/Lambda網頁, 並為我們設定的「GetTemperatureIntent」實作底層的處理函式. 選擇Create建立新的Lambda Function, 並暫時選擇空白的藍圖, 因為我們將使用到外部的函式庫, 例如「request」或「https」等以建立HTTPS/GET請求.
網路上有許多人詢問關於「如何在Alexa Skill建立HTTPS/GET請求」這個議題, 筆者試了很多版本, 只有Kathryn
Hodge的版本最簡單(所需要的外部軟件她也都幫我們打包好了). 請直接下載Archive.zip這個檔案並上傳到Lambda, 並依照下圖的步驟將該ZIP檔上載並按儲存.
檔案上載之後將下面筆者寫的樣板整個複製並貼上即大功告成. 讀者可以修改getWelcomeResponse函式以提供該App使用說明, 該函式由AMAZON.HelpIntent觸發. 而getTemperatureHandler為建立HTTPS/GET請求的主要函式, 其中引用的外部函式庫request為核心來完成. ThingSpek回傳的資料為JSON物件(內容為ESP-01所上傳的最新一筆溫/濕度值), 細部的設定請參閱「以ESP8266存取IoT數據資料庫」文中對於建立Channel與溫/濕度欄位的說明, 其中field1為濕度值(humidity)而field2為溫度值(temperature).
在request.get的參數中, 請讀者填入自己申請的Channel Id與API-KEY(read
mode)與符合自己資料欄位定義的格式以順利建立語音資訊回傳. 除了「End of you code above …」以上的三個函式, 其它程式碼皆不需做任何修改.
// Start your code below
var request = require('request');
// Called when the user
specifies an intent for this skill.
function onIntent(intentRequest,
session, callback) {
var intent = intentRequest.intent;
var intentName = intentRequest.intent.name;
// dispatch custom intents to handlers here
if (intentName == "GetTemperatureIntent") {
getTemperatureHandler(intent, session, callback);
} else if (intentName === "AMAZON.HelpIntent") {
getWelcomeResponse(callback);
} else {
throw "Invalid intent";
}
}
function
getWelcomeResponse(callback) {
var speechOutput = "Welcome! I am ready to access
ThingSpeak.";
var reprompt = speechOutput;
var header = "Query ThingSpeak”;
var shouldEndSession = false;
var sessionAttributes = {
"speechOutput" :
speechOutput,
"repromptText" : reprompt
};
callback(sessionAttributes,
buildSpeechletResponse(header,
speechOutput, reprompt, shouldEndSession));
}
// get info from
ThingSpeak (channel: DHT11)
function
getTemperatureHandler(intent, session, callback) {
var url = "https://api.thingspeak.com/channels/Channel/feeds/last.json?api_key=APIKEY";
// sample return JSON
{"created_at":"xxx","entry_id":xxx,"field1":"59.00","field2":"29.00"}
request.get(url, function(error, response, body) {
//console.log(body);
var d = JSON.parse(body);
var humidity =
Math.round(d.field1);
var temperature = Math.round(d.field2);
var speechOutput = "We have an
error.";
var reprompt = JSON.stringify(d);
if ( humidity>0 ) {
speechOutput = "The
temperature is " + temperature + " degree, and " +
"the humidity is
" + humidity + " %.";
}
callback(session.attributes,
buildSpeechletResponseWithoutCard(speechOutput, reprompt, true));
});
}
// End your code above
/////////////////////////////////////////////////////////////////////////////////////////
// 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);
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) {
// add any session init logic here
}
// Called when the user
invokes the skill without specifying what they want.
function onLaunch(launchRequest,
session, callback) {
getWelcomeResponse(callback);
}
// Called when the user
ends the session.
// Is not called when
the skill returns shouldEndSession=true.
function
onSessionEnded(sessionEndedRequest, session) {
}
// ------- Helper
functions to build responses for Alexa -------
function buildSpeechletResponse(title,
output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
card: {
type: "Simple",
title: title,
content: output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
};
}
function buildSpeechletResponseWithoutCard(output,
repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
};
}
function
buildResponse(sessionAttributes, speechletResponse) {
return {
version: "1.0",
sessionAttributes: sessionAttributes,
response: speechletResponse
};
}
function capitalizeFirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
|
按儲存之後請將網頁編輯器最右上方的ARN碼複製下來, 它將用來綁定ASK的外部服務service endpoint, 它的格式為: 「arn:aws:lambda:us-east-1:xxxx:function:ThingSpeak」
Step4: Configuration
完成Lambda程式並取得ARN碼之後我們須回到ASK的頁面繼續完成configuration, 如下圖, 在service enpoint中勾選AWS Lambda並填入ARN code.
Step5: 測試
ASK提供了文字轉語音輸出的模擬器與Echo語音助理的模擬器, 因此即使我們手邊並沒有真正的Echo硬體設備也能模擬真實的結果. 從模擬器中可以清楚看見底層資料以JSON的遞送/交換過程, 當使用者說出「get
information」, ASK從列舉的utternaces中成功辨識/匹配的「GetTemperatureIntent」並以JSON交由Lambda處理, 最後我們寫的Intent Handler函式處理完之後再以JSON傳回ASK接手後續的Echo發聲與手機文字顯示等服務. 當系統等不到語音指令則「AMAZON.HelpIntent」被觸發, 並交由Lambda/getWelcomeResponse函式處理.
測試步驟:
筆者: Alexa, ask
information
(不給intent, 靜待ASK觸發AMAZON.HelpIntent, Alexa給出welcome訊息)
Alexa: Welcome! I am ready to access ThingSpeak
筆者: get the
temperature
Alexa: The temperature is 29 degree, and the humidity is 59 %.