2017/6/20

用 Alexa 控制 Arduino(ESP8266) -- 實作 Alexa Compliant 恆溫器


照著個態勢看來, 未來能與Amazon相抗衡的可能只有「幾乎甚麼都能買賣」的馬雲+「幾乎甚麼都能製造」的郭台銘聯手了. 試想, 將來創業或許也不用開甚麼公司, 只要有好的創意就有機會在媒合平台上實現商品化並且在支付系統完善的環境下做買賣了, 而這也是目前Amazon很積極正在佈局的一塊拼圖. 這是做到極致的共享經濟, 或許也能解決像筆者這樣仍然還有生產力的中年失業問題呢!

(說到這兒, 也寫了不下十篇文章了, 也幫老外打工當打工仔很久了! 甚麼時後才能搞個中文版的Ecosystem? 畢竟也有十幾億人口不是嗎?)


Alexa Compliant商品
網路上有在販賣許多關於Alexa Compliant商品, 例如基本開關機能的電插座、調色燈泡、電動窗簾以及搭配IrDA裝置的音響、TV、冷氣/恆溫器等設備. 我們不想幫Amazon打廣告, 但希望能站在別人的肩膀上看得更遠, 並能想出更有趣且實用的應用. 例如, 下面是Alexa/濕度計的示意圖, 可以透過Alexa查詢並「說出」目前家裡的溫/濕度值.




本案例將試範(prototype階段)以不超過2元美金(ESP-01LCM6102模組)成本就可以實現支持WiFi版的Alexa Compliant Product(包含Alexa Skill App, 量產後還可以再降低成本), 它將可以是標榜著AI溫度計的高檔貨啊! (不過溫度計可能不好賣, 若能改成特別針對土壤、魚缸或環境之類的或許比較有意義)

本案例除了前案「ESP8266實作Alexa Compliant溫濕度計」被動的溫/濕度讀取功能之外, 又增加了主動對Arduino提供設定溫度的功能, 相信讀者可以很容易地把它改成其它的應用或開關裝置, 例如Alexa插座、Alexa電燈、Alexa馬達(開關窗簾)等等.



長距離低功耗物聯網傳輸協訂
實現這個目的可以透過正規/制式的MQTT協定或REST協定, 資料路徑如下圖, MQTT須要一個中介器(Broker)以傳遞資料, 但相較HTTP而言MQTT是屬於更輕量級的傳輸協訂.



關於兩者之間的比較, 摘要如下表, 有興趣的讀者可以在網路上找到不少資料.



也有許多專家針對兩者的功耗表現進行比較, 摘要如下表(MQTT較省電), 有興趣的讀者請上網參閱「Power Profiling: HTTPS Long Polling vs. MQTT with SSL, on Android.




若我們希望透過AlexaArduino發號施令, 則我們需要提供另一條資料路徑以達到「雙向」與「主動」控制的目的(雖然實做上仍然是單向).

MQTT Protocol
資料上傳: ESP-01(扮演Publisher)DHT11資料(Tpoic)發佈到IoT資料庫(扮演Broker), AWS/Lambda(扮演Subscriber)訂閱DHT11資料(Topic). 一旦ESP-01上傳新資料, Broker即將資料發佈到AWS/Lambda(此時監看port 1883, 有現成函式庫).

接收命令: AWS/Lambda(扮演Publisher)Alexa命令(Tpoic)發佈到IoT資料庫(扮演Broker), ESP-01(扮演Subscriber)訂閱Alexa命令(Topic). 一旦AWS/Lambda上傳新命令, Broker即將命令發佈到ESP-01(此時監看port 1883, 而我們所開發的ESP-01 AT指令也要依標準MQTT協定).

REST Protocol
資料上傳: ESP-01模組設為STA模式(HTTP Client), DHT11資料經由REST API送給IoT資料庫. AWS/Lambda同樣以REST APIIoT資料庫查詢(非同步).

接收命令: ESP-01模組設為AP模式(扮演HTTP伺服器), 並提供REST API以等待AWS/Lambda(Client)或其他中介IFTTT發出的連線請求. 但此運做模式ESP-01模組必須在WAN, 也就是要有固定IP並寫伺服器端應用程式(此時監看port 80, 而我們所開發的ESP-01 AT指令與REST API也要依標準HTTP協定).



Alexa恆溫器工作原理(非正規)
相較上述的方法, 本例與IoT資料庫之間的資料路徑介於兩者之間(屬於非制式/非正規的做法).

資料上傳: 於前例「ESP8266實作Alexa Compliant溫濕度計」中, 我們已知道透過ESP-01(STA模式)發出HTTPS/GET請求以將最新的溫/濕度測量值放上ThingSpeak資料庫的DHT11 Channel, 只要透過AWS/Lambda程式對ThingSpeak的該Channel發出HTTPS/GET請求, 即可將最新的溫/濕度測量值透過Alexa語音助理(Echo Dot)「說出來」. 資料路徑如下圖藍色箭頭, 它事實上是「被動」(等資料上傳)且「單向」的




接收命令: 我們仍然把ESP-01模組設為STA模式(HTTP Client)並採取「間歇式」對ThingSpeak資料庫(同樣在WAN)」的Alexa Channel做訪問與資料讀取(此行為類似Subscribe). 此時, AWS/Lambda只要將指令上傳到ThingSpeak資料庫的該Channel即可(此行為類似Publish), 無須透過其他中介或IFTTT服務. 對照上圖橘色箭頭, 實現了一個虛擬的資料路徑: 直接由Alexa語音助理(Echo Dot)控制Arduino (ESP-01).

本例採用ESP8266AT命令實做: 「概念上」像直接對IoT資料庫做Publish(上傳DHT11資料)Subscribe(訂閱Alexa Channel的命令, 由於沒有Broker, 我們跑到網外去跟Server), 但並不是真的完整實做HTTPREST protocol, 如下圖所示.




相較MQTTPublish/Subscribe架構, 本例在實做上AlexaIoT資料庫的存取是由「語音事件」並成功產生Intent時才觸發的. 期間即使ESP-01更新資料, ThingSpeak並不需要告知所有Client. ESP-01則是採間歇式對ThingSpeak做資料更新與讀取, 期間即使AWS/Lambda更新資料, ThingSpeak並不需要告知ESP-01模組. 因為沒有任何伴隨資料更新的Publish動做, 相較耗電量不一定會輸給資料更新頻率高的案例.

本例中的「IoT資料庫」仍然選用業界最簡單的ThingSpeak, 我們刻意不去使用Amazon IoT仍然能達到目的(或許比較耗電, 畢竟每次loop都跑到網外去了). 若一切事情(Things)都架設在Amazon站內一來很可怕, 二來對整個Ecosystem也很不健康, 總是要讓別人有機會共創價值吧? 近來MATLAB也面臨強大的open source與硬底子大軍Scikit-learn」的挑戰, 營運模式也不斷在進行改變, 因此後續發展我們也密切觀察中.



新增一個Alexa Channel(附帶自己的驗證碼)
有別於前案的DHT11 Channel存放的是經由ESP-01所上傳(發佈出來)DHT11/濕度值, 我們需要新增一個Alexa Channel來存放經由AWS/Lambda所上傳(發佈出來)針對Arduino的控制項. 由於上傳一筆新資料到ThingSpeak只須提供一組APIKEY, 方法雖然簡單方便, 但這對於要求安全且嚴謹的「控制」來說可不是好事. 因此, 我們利用第一個資料欄位當我們控制項的安全驗證碼, 也就是當Arduino透過ESP-01Alexa Channel抓回來的控制項須經過比對符合我們自訂的安全碼時才能有動做(避免別人誤寫到我們家Channel). 其它細部設定請參考「ESP8266存取IoT數據資料庫.





ASKAWS/Lambda程式架構
與前案「AlexaThingSpeak讀取溫度-發出HTTPS GET請求」相同, 但增加對Arduino做溫度設定的意圖(SetTemperatureIntent). 關於ASK的細項設定與建立步驟請參考前案.

ASK/Intent Schema
以前案為基礎, 我們添增一個稱為「SetTemperatureIntent」的意圖, 並使用內建的NUMBER資料型別來儲存所設定的溫度, 相關細節請參考「Alexa記得你-Alexa Skill的變數存取.

{
  "intents": [
    {
      "intent": "GetTemperatureIntent"
    },
    {
      "slots": [
        {
          "name": "tempValue",
          "type": "NUMBER"
        }
      ],
      "intent": "SetTemperatureIntent"
    }
  ]
}



ASK/Sample utterances
為了讓Alexa能成功辨識新增的使用者語音指令「set temperature …」並觸發我們所定義的「SetTemperatureIntent, 因此我們先預想幾種可能的使用者語音指令, 以成功觸發該「意圖(Intent)」例如:

GetTemperatureIntent get the temperature
GetTemperatureIntent what is the temperature
GetTemperatureIntent give me the temperature
GetTemperatureIntent query the temperature
SetTemperatureIntent set the temperature to {tempValue}
SetTemperatureIntent set the temperature to {tempValue} degree
SetTemperatureIntent set temperature to {tempValue}
SetTemperatureIntent set temperature to {tempValue} degree



AWS/Lambda程式
Lambda程式延用前案「AlexaThingSpeak讀取溫度-發出HTTPS GET請求, 底下範例只顯示新增關於溫度設定的處理函式(setTemperatureHandler), 請填入Alexa ChannelAPIKEY與自訂的驗證碼(field1), 本例設定為field1=Alexa. 其它的程式碼與前案相同.

// 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 == "SetTemperatureIntent") {
        setTemperatureHandler(intent, session, callback);
    } else {
         throw "Invalid intent";
    }
}

function setTemperatureHandler(intent, session, callback) {
    const slot = intent.slots.tempValue; // tempValue
    const value = slot.value;
    var url = "https://api.thingspeak.com/update?key=APIKEY&field1=Alexa&field2=" + value;
    request.get(url, function(error, response, body) {
        var cnt = JSON.stringify(body); // entry Id
        cnt = cnt.replace(/\"/g, "");
        speechOutput = "The temperature was set to " + value + " degree.";
        reprompt = speechOutput +
            " There are total " + cnt + " row data in the database.";
        callback(session.attributes,
            buildSpeechletResponseWithoutCard(speechOutput, reprompt, true));
    });
}





Arduino系統接線
細部接線與前案「ESP8266 實作Alexa Compliant溫濕度計」完全相同.





Arduino程式
筆者的Arduino IDE開發環境是1.8.2 ZIP免安裝版. 程式部份我們基於前案「ESP8266實作Alexa Compliant溫濕度計, 仍然是完全透過AT命令達成沒有引用其它WiFi函式庫. ESP-01Alexa Channel的資料讀取仍然透過queryThingSpeak函式, 但請改成對應的Alexa ChannelAPIKEY. 針對ThingSpeak資料庫的回應, 新增了parseJSON函式, 但本例筆者(偷懶)只是用最快的方式抓由Alexa上傳的溫度設定(放在field2欄位)並不是真的parse完整的JSON資料框

我們假設網路傳輸過程並非萬無一失, 而欄位field1保留給資料驗證用, 必須是”Alexa”字串才是正確的控制項(相當於scbscriber選擇訂閱的topic). 如前面所述, 我們並非實做完整的HTTP資料框架/協定. 有興趣的讀者可以仔細的觀察ESP-01的緩衝區佇列裏面的資料內容, 其實包含了尚未去除HTTP資料框架的字元, 例如+IPD,client_id:{JSON}CLOSED




// sample message received through ESP-01
// +IPD,82:{"created_at":"2017-07-15T15:27:35Z","entry_id":24,"field1":"Alexa","field2":"22"}CLOSED
float parseJSON(String inMsg) {
    float value = 0; // temperature value (0 indicates error)
    char* msg = inMsg.c_str();
    char* p1;
    char* p2;

    if ( (p1=strstr(msg,"+IPD"))==NULL || (p2=strstr(msg,"CLOSE"))==NULL ) {
        Serial.println("Error! fail to fetch JSON data.");
        return 0;
    }
    if ( (p1=strchr(msg,'{'))==NULL ) {
        Serial.println("Error! fail to fetch JSON data.");
        return 0;
    }
    msg = p1;
    *(p2) = '\0'; // end JSON

    // fetch temperature value in JSON frame
    // sample ... "field2":"22"}
    if ( (p1=strrchr(msg,':'))!=NULL ) {
        msg = p1+2;   // trim the 1st bracket
        *(p2-2) = '\0';   // trim the 2nd bracket
        value = String(msg).toFloat();  // field2 := temperature
    }
    return value;
}



loop迴圈, 我們每次更新資料到ThingSpeakDHT11 Channel即透過LCD顯示內容. 之後我們將LCD熄滅, 並刻意等待約5秒以接受對Alexa的語音指令, 其目的僅做展示效果用, 實務上這部份是多餘的(可以將程式碼移除), 因為在本例中語音指令與執行是非同步的.

void loop() {
  #if DEBUG // ESP-01 debug by using AT command through serial console
    if ( Serial.available() ) {
        ESP.write( Serial.read() );
    }
    if ( ESP.available() ) {
        Serial.write( ESP.read() );
    }
  #else
    if ( swState && wifiState && dataCnt < 100 ) {
        // get DHT11 data
        float h = dht.readHumidity(); // reading temperature or humidity
        float t = dht.readTemperature(); // read temperature as Celsius (the default)
     
        lcd.backlight();
        lcd.clear();
        lcd.setCursor(0, 0); lcd.print(String(h) + "%"); // humidity
        lcd.setCursor(0, 1); lcd.print(String(t) + "C, DHT11"); // temperature
       
        // update humidity and temperature to ThingSpeak
        Serial.println("update data to ThingSpeak: " + String(++dataCnt) );
        updateThingSpeak(
            "184.106.153.149",  // ThingSpeak IP
            Write_APIKEY,     // write API-KEY (DHT11 channel)
            h,   // humidity
            t    // temperature
        );
        setDelay(3000);
        lcd.noBacklight(); // turn off back light
       
        setDelay(5000); // wait for voice command (from Echo Dot)
        // For example: Alexa, ask information, set temperature to 27
       
        // query data from ThingSpeak through Alexa channel
        Serial.println("query data from ThingSpeak through Alexa channel...");
        String msg = queryThingSpeak(
            "184.106.153.149",  // ThingSpeak IP
            Alexa_Channel,    // channelId (Alexa channel)
            Read_APIKEY     // read API-KEY (Alexa channel)
        );
       
        // set temperature command form Alexa (Echo Dot)
        float value = parseJSON(msg); // fetch temperature from JSON
        lcd.backlight();
        lcd.setCursor(0, 1); lcd.print(String(value) + "C, Alexa");
        setDelay(3000);
        lcd.noBacklight(); // turn off back light
       
        Serial.println("press switch to stop (will start after one minute) ...");
        if ( swState )
            setDelay(1000); // update humidity and temperature 10 second
    }
  #endif
    setDelay(500); // monitor hardware interrupt
}








測試步驟:
(: 溫度是由DHT11所量測到的, 並顯示在LCD螢幕, 其中右下角顯示DHT11. 當時溫度29C, 濕度59%. 靜置3LCD熄滅, 以進行Alexa語音指令測試)

筆者: Alexa, ask information, set temperature to 25
(: information是筆者寫的Alexa Skill, 以將設定溫度放上ThingSpeak)

Alexa: The temperature is set to 25.
(: 接收語音指令之後, LCD亮起, 並顯示Arduino所接收到的新溫度設定25C, 其中右下角顯示Alexa)
(: LCD熄滅, 再進行第二次測試)

筆者: Alexa, ask information, set temperature to 20.
Alexa: The temperature is set to 20.
(: 接收語音指令之後, LCD亮起, 並顯示Arduino所接收到的新溫度設定20C, 其中右下角顯示Alexa)

(: 相信讀者將本案例改成Alexa電插座、Alexa燈泡或Alexa電窗簾是輕而易舉了)