當 Alexa 遇上 ESP8266 (一)
去年的 AWS 的 re:Invent 上,我見識了 Alexa Voice service 的本事後深受震撼,我除了帶了幾顆 Echo 和 Echo Dot 回來研究外,也想著要用 ESP8266 來做可以讓 Alexa 控制的裝置。
其實回來之後沒多久我就做出來了,但這篇文章一直沒寫。等到我想寫文章、把寫好的程式再度拿出來看時,發現我所使用的 library 已經大幅更新,我原來寫的程式根本沒辦法 compile,只好整個砍掉重練。
時隔一年,這幾天正值 re:Invent 2017 進行中。今年因為種種因素我無法成行,就用這篇文章來紀念去年此時的這個 idea 和那時的熱血吧。
偽裝
要做一個讓 Alexa 可以控制的裝置有很多方法,但大部份的方法都要用到 AWS 的 IoT 或 serverless 服務,至少要寫一點 Lamda script 去處理 Alexa 跟裝置間的互動,或者要走 MQTT 透過 IFTTT 之類的服務來運作。一但用了 backend server 的服務,被控裝置的安裝就要處理註冊和認證等問題,操作上會變得很麻煩。
尋尋覓覓後,我發現有另一種更聰明的做法: 偽裝。
Alexa 支援一系列的智慧插座,可以用 Alexa 控制插座電源的開或關,而這其中最有名的就是 Belkin 出的 Wemo Switch。Wemo Switch 必需跟執行 Alexa 的 Echo 或 Echo Dot 位在同一個 WiFi 網路之下才能運作,因此就有人推測它們倆可能是在 LAN 上直接溝通,而不經過雲上的 server。
有一位很有名的 maker,Chris Derossi,利用 Wireshark 這個老牌的 network sniffer 監聽 Echo 跟 Wemo Switch 之間的通訊後,解開了這個謎。Echo 跟 Wemo Switch 都沒有 Ethernet 接口,因此只能用 WiFi sniffer 來監聽。這張圖發表在他原來的部落格文章 Amazon Echo and Home Automation 中:
原來 Echo 跟 Wemo Switch 之間是這麼運作的:
- 當使用者第一次設定 Wemo Switch 時,要先對 Echo 喊 “Alexa, descover devices”。此時 Echo 會利用 UDP broadcast 送出一個 UPnP 的 SSDP 請求,SSDP 是 UPnP 控制器用來在網路上尋找裝置用的協定,它是一個 HTTP request,帶有一種叫 M-SEARCH 的 method,只有 header 沒有 body,送往 IPv4 的 multicast 地址 239.255.255.250:1900。
- 同一個網路上的 Wemo Switch 都聽得到 IP multicast。它們發現這個 HTTP request 是在叫自己後,就會回傳一個 HTTP response 給發出 IP multicast 的 Echo,裡面最重要的訊息是一個 URL。這個 URL 指向一個 XML 檔,用來描述裝置本身的屬性。這時的回覆會來自網路上的多個裝置,因為每個收到 IP multicast 的 Wemo switch 都會回答,所以網路會有一小段時間變得很吵。以上都還是走 UDP,所以不需建立連線。
- Echo 收到這些回覆的 URL 後,依序對每個裝置發出 HTTP request,讀取每個裝置的 XML 配置檔。這時的協定通訊協定改走 TCP,會先建立連線,傳送的資料也會變得可靠。
- 每個裝置用 HTTP response 回覆它們的設定檔,也是走 TCP。這個設定檔很長,是一個一百多行的 XML 檔,但其中只有一欄比較重要,就是這個裝置的 friendly name。Friendly name 就是我們幫每一個 Wemo Switch 取的名字,當我們要用 Alexa 的語音服務控制它時,叫的就是這個名字。
- 設定完成後,我們會在 Alexa 的 app 中看到它已經找到的裝置列表,此時就可以用語音控制每個 Wemo Switch 的開或關。
- 當使用者喊出語音指令且 Alexa 聽懂後,它會送出一個 HTTP request 給對應的裝置,裡面有一大串有的沒有的資料,甚至包含了整組包在 XML 裡的 SOAP 指令,但其實我們真正關心的就只是 <BinaryState> 這個 tag 而已,而這個 tag 只有 1 跟 0 兩個值。
- Wemo Switch 收到指令並確認動作後,會回應一個 HTTP response,這是一個固定內容的 acknowledgement。
- Echo 收到確認的回覆後,才會講 “OK”。
Chris Derossi 隨後在另一篇 post 中發佈了一支 Python 程式,用來模擬 Wemo Switch 並回應來自 Alexa 的這些訊息。這支程式叫 fauxmo,有點向 Wemo 致敬的味道。
Python or Not Python?
fauxmo 是用 Python 寫的,因此要在 ESP8266 上執行,就得讓 ESP8266 跑 Python。
ESP8266 有個 MicroPython 的執行環境,但我只試過一些簡單的功能,非常不熟。(後來我看到有人在 ESP32 上做了這件事: 把 Fauxmo 整包移植到 ESP32 的 MicroPython 上執行。詳情在 #MicroPython: WeMos and Amazon Echo 這篇文章。)
太陽底下沒有新鮮事,我苦惱的事一定也困擾著地球上的某人,於是我找到解答了。一位叫 Xose Pérez 的西班牙駭客把 fauxmo 移植到 C++ 上,並把它包成 ESP8266 的 Arduino library。這個 library 叫 fauxmoESP。
一年前我剛開始這個實驗時,fauxmESP 還是 1.0.0 版 (2016/11/28 release,就在我開始這個專案前沒幾天),但今年年中再度試著啟動板子時,fauxmoESP 已經改版到 2.1.0,並且經歷了非常大的翻修。
2.1.0 版最大的變化就是作者用 ESPAsyncTCP 這個非同步的 TCP/IP library 取代了 ESP8266 Arduino core 裡的 TCP/IP stack,並並利用 ESPAsyncWebServer 這個非同步架構的 web server 來處理 http request,用以取代 ESP8266 Arduino core 裡的 web server class。ESP8266 Arduino core 裡的 web server 雖然簡單,但是要為每一個 URL 寫一個 callback function,而且還要自己想辦法在 callback function 之間串流程,用起來其實很麻煩。
但這次改版最困擾我的是,我很愛用 WiFiManager 來設定 ESP8266 的 WiFi 連線參數,但 WiFiManager 裡用來顯示 captive portal 的 web server 用的是 ESP8266 Arduino core 裡的那一個,跟 ESPAsyncWebServer 不相容。難道我得放棄 WiFiManager 嗎 ?
太陽底下還是沒有新鮮事,我苦惱的事一定也困擾著地球上的另一個苦主。有個也很愛用 WiFiManager 的人也遇到了 AsyncTCP library 的問題,因此他就把 WiFiManager 移植到 ESPAsyncWebServer 上。修改過的 WiFiManager 在這裡。我愛開源社群。
於是經過一番努力,我又把 fauxmoESP 跑起來了。但我還是沒動手寫文章。
幾個月又過去了,幾天前我要開始寫這篇文章時,我發現 fauxmoESP 又改版了,這次變成 2.3.0,今年 11/8 發佈的。
我看版次更新不大,就沒認真看 changelog 直接把 library 更新,結果一更新後程式連 compile 都過不了,fauxmo 物件缺了一大堆 method,我在原來程式碼中呼叫的 method 都不見了!
仔細看了 changelog,裡面這樣說:
Deprecated
- Use onSetState callback instead of onMessage callback
- Use onGetState callback instead of setState method
原來 callback function 的名字改了,參數也改了。
我照新版的 library 把程式碼修改好,它又可以動了。因此這邊要特別提醒讀者,如果你是多年後才看到這篇文章,照作者改版的速度,那時的 fauxmoESP 可能已經跟這篇文章中所描述的不相容,請務必注意。
來看程式碼吧!
fauxmoESP 用起來其實非常簡單。
#include <fauxmoESP.h> fauxmoESP fauxmo;
這個 library 只有一個叫做 fauxmoESP 的 class。使用前,要先宣告一個這個類別的物件。
fauxmo.addDevice("switch one"); fauxmo.addDevice("switch two");
接著,在 Arduino 的 setup() 中,用 .addDevice 這個 method 告訴它裝置的名字。Chris 很聰明地發現了 Echo 是透過完整的 URL 包含 port number 來識別裝置,因此事實上一個 IP address 可以模擬多個 Wemo Switch 裝置,我們可以多次呼叫 .addDevice 增加裝置。
.addDevice 的參數就是裝置的 friendly name,也就是我們跟 Alexa 下指令時要叫的名字。這個名字可以不只一個英文單字,它可以像是 “front door light” 這樣的名字。我試過 “the second light in front of the desk” 這麼長的名字,Alexa 也能正確識別,很厲害。
接下來稍微複雜一點,要用 “onSetState” 這個 method 註冊一個個 callback 函數。
fauxmo.onSetState([](unsigned char device_id, const char * device_name, bool state) { Serial.printf("Device #%d (%s) state: %s\n", device_id, device_name, state ? "ON" : "OFF"); });
onSetState 這個 method 要傳一個 callback function 給它,這個 function 會在模擬的 Wemo Switch 被 Alexa 要求設定狀態時被呼叫。fauxmo 物件會傳三個參數給這個 callback function:
- unsigned char device_id: 裝置的編號
- char * device_name: 裝置的名字,就是 friendly name
- bool state: 要設定的狀態
其中 device_id 是在 fauxmoESP 2.1.0 版才引入的參數。當使用者在同一個 ESP8266 上註冊多個裝置時,在 callback function 被呼叫時,要想辦法識別這個狀態設定是針對哪一個裝置。在 2.1.0 版之前因為只有 device_name,要比對字串才能知道是哪個裝置,會寫 C 語言的人都知道 C 的字串處理是有名的難用,因此作者在 2.1.0 版開始提供 deivce_id,讓我們可以直接看數字就知道是哪個裝置被呼叫。
上面的範例用了C++ 的匿名函數寫法,因此被呼叫的 calback function 沒有名字,你也可以老老實實在其他地方定義這個 callback function,再用 function pointer 傳給 onSetState。
最後,只要在 main loop 中定期呼叫 .handle 這個 method,讓物件有機會拿到 CPU time 去處理呼叫就可以了。
void loop() { fauxmo.handle(); }
就這樣,我們完成了用 ESP8266 模擬 Wemo Switch 讓 Alexa 控制的工作。
唯一美中不足的是,因為 Wemo Switch 真的就只是個 switch,因此 Alexa 只設計了傳遞 on/off 兩個狀態給它,沒辦法傳遞更多的訊息。即使我們能用 ESP8266 產生 PWM 或是做更複雜的動作,Alexa 仍然只能傳 on/off 兩個命令給它。
只有 on/off 兩個狀態能做什麼事呢 ? 大概只能拿來控制電源開關了。因此接下來我打算做一片板子,用 ESP8266 去拉一顆 relay 或 SSR 來控制 AC110V 的電源。除此之外,還可以用 consumer IR 介面來遙控其它的電器,但 CIR code 要怎麼來需要想一下,可能接個 38KHz CIR decoder 直接用學的會比較簡單。
(未完待續)
數據之間的反應速度如何,放一段時間後心跳會不會停止連線?