2017/6/18

以ESP8266 實作 Alexa Compliant 溫濕度計


本例標題看起來似乎不凡, 好像是能與目前引領潮流的Alexa語音助理兼容的AI高檔貨呢? 但事實上我們啥事都沒幹! 只不過延續前一個案例「ESP8266存取IoT數據資料庫, 即我們知道怎麼用ESP-01透過REST APIDHT11/濕度值上傳ThingSpeak資料庫(並做讀取確認)之後, 改良一下Arduino程式再加個LCD顯示器罷了! 而關於Alexa Skill部份, 請參考前一個案例AlexaThingSpeak讀取溫度-發出HTTPS GET請求.


基本概念
整個概念如下圖所示, 我們外加一個LCD顯示, 再透過ESP-01發出HTTPS/GET請求並將最新的溫/濕度測量值放上ThingSpeak IoT資料庫的某個Channel. 因此在Alexa Skill Kit底層只要透過AWS/Lambda程式對該Channel發出HTTPS/GET請求, 即可將最新的溫/濕度測量值透過Alexa語音助理(Echo Dot)「說出來」. 其中Echo設備已連接WAN, 因此並不侷限在自己家裡







為系統增加一個 LCD 螢幕
所有硬體以前案「ESP8266存取IoT數據資料庫」為基礎再增加一個LCM6102模組, 整個系統如下圖所示. 關於LCD的驅動程式請參閱前例「LiquidCrystal_I2C (LCM6102) 顯示器」的測試程式, 該驅動程式可以在Arduino IDE v1.8.2版本運作良好.




如開頭的標題, 我們想要實作一個Alexa Compliant溫度計, 所以我們可以將醜醜的外觀稍為美化/包裝一下, 如下圖. 它是標榜著AI商品的高檔貨啊! 左下方透露的一些紅光即ESP-01模組. 筆者算了一下, 增加成本應該可以不超過2塊美金(ESP-01LCM6102模組).




細部接線
系統接線融合了「ESP8266存取IoT數據資料庫」與「LiquidCrystal_I2C (LCM6102) 顯示器」兩者, 新增的LCM6102模組接線部份為: GND(Arduino GND)VCC(Arduino 5V輸出)SDA(Arduino A4)SCL(Arduino A5). 因此整個系統接線看起來如下圖, 其中省略了液晶顯示面板部份.






Arduino程式
筆者的Arduino IDE開發環境是1.8.2 ZIP免安裝版. 程式部份我們基於前案「ESP8266存取IoT數據資料庫, 仍然是完全透過AT命令達成, 因此沒有引用其它WiFi函式庫. 但相較於前案, 又再次做了整體的改良(其實是修正了一些之前預留的BUG). 其中connectWAN函式中請填入自己家中APSSIDPASSWORD, 而該函式一旦連線失敗時, 我們仍然可在Serial監控窗透過AT命令訪問ESP-01以釐清問題.

我們也將訪問ThingSpeak資料庫所需的REST請求(HTTPS/GET)打包成queryThingSpeakupdateThingSpeak兩個函式, 其中包含了所需的ESP-01 AT命令與步驟以建立HTTPS/GET請求.

#include <SoftwareSerial.h>
#include "DHT.h" // package required from https://github.com/adafruit/DHT-sensor-library
// !NOTE! You may need to enlarge the serial RX buffer size to 128 for baud rate 115200bps, described in the
// include file arduino-1.8.2\hardware\arduino\avr\libraries\SoftwareSerial\src\SoftwareSerial.h, e.g.,
// #define _SS_MAX_RX_BUFF 128 // software serial RX buffer size (default is 64 bytes)

// This driver works well with Arduino v1.8.2
#include <LiquidCrystal_I2C.h>

#define DEBUG 0

DHT dht(5, DHT11); // digital pin5 connected to the DHT11
SoftwareSerial ESP(3, 2); // ESP8266 ESP-01: Tx, Rx

// Addr, En, Rw, Rs, d4,d5,d6,d7 backlight, polarity
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); // LCD1602 with I2C

bool swState = 0;   // switch to enable/disable hunidity and temperature update
bool wifiState = 0;  // check wifi connection
int  dataCnt = 0;   // times to access ThingSpeak IoT database

void setup() {
    Serial.begin(115200);
    pinMode(7, INPUT_PULLUP);  // set pin7 as the switch button

    // initialize LCD
    lcd.begin(16, 2); // init LCD : 2 rows x 16 columns
    lcd.backlight();
    lcd.clear();
    lcd.print("Hello World!");
    delay(2000);
    lcd.noBacklight(); // turn off back light and wait for user input

    // initialize DHT11
    dht.begin();
    delay(1000); // Sensor readings may be up to 2 seconds
    Serial.println("DHT11 start ...");

    // initialize ESP-01
    ESP.begin(115200);
    delay(5000);
    Serial.println("ESP-01 start ...");

  #if !DEBUG
    Serial.println("try to connect ESP-01 to WAN (through home AP) ...");
    if ( connectWAN(SSID, PASSWORD) ) { // join ESP-01 to WAN through your home AP
        wifiState = 1;
        Serial.println("press switch button to start ...");
    } else {
        Serial.println("fail to join WAN ...");
    }
  #endif
}

bool connectWAN(char* ssidName, char* passWord) {
    int status = false;
    String joinCmd = "AT+CWJAP=\"" + String(ssidName) + "\",\"" + String(passWord) + "\"\r\n";
   
    sendATcmd("AT+RST\r\n", 2000, DEBUG);      // reset ESP-01
    sendATcmd("AT+CWMODE=1\r\n", 1000, DEBUG); // config ESP-01 as STA mode
    sendATcmd(joinCmd, 1000, DEBUG);           // wait the server to ack "OK"
   
    for (int loop=0; loop<10; loop++ ) { // wait AP's response
        Serial.print(".");
        if ( ESP.find("OK") ) {
            status = true;
            Serial.println("join ESP-01 to WAN success ...");
            break;
        }
        setDelay(1000);
    }
    return status;
}

String sendATcmd(String cmd, unsigned int msDelay, bool dbg) {
    String espMsg = "";
    unsigned long timeout = millis() + msDelay;
 
    Serial.print(">>ESP-01: " + String(cmd)); // debug
    ESP.print(cmd); // send AT command to ESP-01
 
    // allow delay > msDelay if ESP-01 still receives message
    while ( ESP.available() || millis()<timeout ) {
        while ( ESP.available() ) {
            char ch = ESP.read();
            espMsg += ch;
            if (dbg) Serial.write(ch); // debug only
        }
    }
    return espMsg;
}

// query data from ThingSpeak
String queryThingSpeak(char* ipName, char* channelName, char* apiKey) {
    String espResp; // receive response from ThingSpeak
    String tcpCmd = "AT+CIPSTART=\"TCP\",\"" + String(ipName) + "\",80\r\n";
    String getCmd = "GET /channels/" + String(channelName) +
                    "/feeds/last.json?key=" + String(apiKey) + "\r\n";
    String cipCmd = "AT+CIPSEND=" + String(getCmd.length()) + "\r\n";

    sendATcmd(tcpCmd.c_str(), 1000, DEBUG); // ask HTTP connection, and wait the server to ack "OK"
    sendATcmd(cipCmd.c_str(), 2000, DEBUG); // start CIPSEND request, and wait the server to ack "OK"
    espResp = sendATcmd(getCmd.c_str(), 2000, DEBUG); // send GET request and receive response
    return espResp;
}
// sample message received through ESP-01
// +IPD,82:{"created_at":"2017-07-15T15:27:35Z","entry_id":24,"field1":"Alexa","field2":"22"}CLOSED

// update data to ThingSpeak
String updateThingSpeak(char* ipName, char* apiKey, float humidity, float temperature) {
    String espResp; // receive response from ThingSpeak
    String tcpCmd = "AT+CIPSTART=\"TCP\",\"" + String(ipName) + "\",80\r\n";
    String getCmd = "GET /update?key=" + String(apiKey) +
                    "&field1=" + String(humidity) +
                    "&field2=" + String(temperature) + "\r\n";
    String cipCmd = "AT+CIPSEND=" + String(getCmd.length()) + "\r\n";

    sendATcmd(tcpCmd.c_str(), 1000, DEBUG); // ask HTTP connection, and wait the server to ack "OK"
    sendATcmd(cipCmd.c_str(), 2000, DEBUG); // start CIPSEND request, and wait the server to ack "OK"
    espResp = sendATcmd(getCmd.c_str(), 2000, DEBUG); // end GET request and receive response
    return espResp;
}

void setDelay(unsigned int ms) {
    unsigned long timeout = ms + millis();
    while ( millis()<timeout ) {
        checkSwitchButton();
    }
}
bool checkSwitchButton() {
    if ( digitalRead(7)==LOW ) {
        swState = !swState;
        Serial.println("\nInterrupt: Switch Button = " + String(swState) );
        delay(300); // debounce delay (depending on your behavior)
    }
    return swState;
}

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 takes about 250 milliseconds
        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
        );             
      #if 0 // query data from ThingSpeak
        Serial.println("query data from ThingSpeak channel...");
        String msg = queryThingSpeak(
            "184.106.153.149",  // ThingSpeak IP
            channelID,          // channelId (DHT11 channel)
            Read_APIKEY // read API-KEY (DHT11 channel)
        );
      #endif
     
        Serial.println("press switch to stop (will start after one minute) ...");
        if ( swState )
            setDelay(10000); // update humidity and temperature 10 second
    }
  #endif
    setDelay(500); // monitor hardware interrupt
}



 
每建立一次資料更新(updateThingSpeak)的請求筆者預設約5000ms時間延遲, 讀者可以自行調整時間, 也以可以將筆者註解掉的查詢(queryThingSpeak)請求在編譯時打開, 以確保所建立的資料完整性. 迴圈大約等待10秒之後才會再次重新建立ThingSpeak的訪問, 期間也可透過硬體中斷暫停, 並在上傳最多100筆資料之後結束.







測試影片
筆者: 根據目前DHT11的讀數, 溫度是31, 濕度是58%.
(: Arduino程式以每隔約15秒更新溫濕度值到ThingSpeak一次)

筆者: Alexa, ask information
(: information是筆者寫的Alexa Skill以存取ThingSpeak)

Alexa: Welcome! I am ready to access ThingSpeak.

筆者: get the temperature
Alexa: The temperature is 31 degree, and the humidity is 58 %.
(: 答案正確, 天氣真的好熱啊! …)