寫好 ESP WiFi 設定程式庫,再也不必為了是否要把 WiFi SSID 跟密碼寫進程式天人交戰,在 ESP8266/ESP32 寫 Web 介面控制硬體的基本框架成形,馬上來個小練習。

四年前買新冰箱,當時曾用 DHT11 + Raspberry Pi + Python 搞過 24 小時溫度監控,資料部分則是送上雲端資料蒐集服務。這次我打算試試用 100 元的 ESP8266 開發板復刻當年的功能,嘗試超省錢的 IoT 解決方案。

開始前先整理本次用到的技巧跟背景知識:

  • DHT11 是最常見的溫溼度偵測元件,量測範圍跟準確度雖不如 DHT22,但超級便宜(一顆 40 元有找),大家玩 Arduino 做實驗幾乎都是用它。Arduino 開源界的好心員外 - Adafruit 有為它寫了程式庫,加入專案:

    再照著範例寫,非常容易上手。

    DHT22 與 DHT11 程式介面完全相同,需要更高準確度的場合將感測器換成 DHT22,程式可不用改。(只有一個地方要注意,DHT22 需要的測量間隔較長,Adafruit 的範例有示範如何自動調整)
  • 一般常見的 ESP8266/ESP32 開發板都有內建 4MB Flash。以 ESP8266 為例,4MB Flash 最前方依序為 64KB 開機程式、4KB ROM Code (有需要時才載入 RAM 的函式庫)、428K 應用程式碼與資料、之後的空間可自由使用,例如拿來模擬檔案系統讓程式寫入或讀取檔案。參考:ESP8266 記憶體分配及管理 by 史坦利Stanley程式Maker的部落格
    至於 ESP32,Flash 還會多一區供 OTA (Over The Air,透過 WiFi 上傳更新程式)使用,但剩下空間一樣可用來建立檔案系統當磁碟機使用。參考:Partiion Tables of ESP32 in PlatformIO
  • Arduino 常用的檔案系統規格有兩種:SPIFFS 及 LittleFS,二者比較可參考 RTOS文件系統對比:LittleFS Vs. SPIFFS by CodingNote.cc。LittleFS 比較新,效能及可靠度都勝過 SPIFFS,而在 ESP8266 Arduino SDK,SPIFFS 甚至已被標示為過時,建議改用 LittleFS;但尷尬的是,ESP32 Arduino SDK 尚未內建 LittleFS,官方預設檔案系統仍是 SPIFFS,如要引用需依賴第三方程式庫。本次專案我想通吃 ESP8266 跟 ESP32,但在 ESP32 啟用 LittleFS 不順利,在 ESP8266 又不想屈就 SPIFFS,算起來也是沒事找事,最後我寫了 #ifdef ESP32 針對不同平台 #include 不同程式庫,並透過 #define WebFS SPIFFS 跟 #define WebFS LittleFS 讓兩種平台使用同一別名存取自己的檔案系統,LittleFS 與 SPIFFS 介面相容,透過這層抽象化就能共用程式碼。
  • ESPAsyncWebServer 網站伺服器程式庫概念上非常先進,使用起來也很簡便,大推! 透過 on("/url-path", HTTP_GET ...) 定義各網址回應邏輯,支援從檔案系統取檔,網頁還可內嵌 %VAR_NAME% 做範本套表,非常好用。
  • 由於 ESP 不是完整的多緒執行環境,實務經驗裡 server.on("/url-path", HTTP_GET, [](AsyncWebServerRequest *request) ) 進行複雜或耗時動作,易出現無法預期結果甚至當機重開,故建議將這類工作移到 void loop() 處理。
  • ESP 的 SDRAM 有限,字串如宣告成 String s = "text text text" 會佔用寶貴 SDRAM 記憶體空間,故可寫成 const char indexHtml[] PROGMEM = "...",宣告 PROGMEM 將其留在 Flash,使用時則要改用 pgm_read_byte_near() 讀取,ESPAsyncWebServer 有提供 beginResponse_P() 等 _P 版本讀取 PROGMEM 字串。另外,Aruduino 也提供了 F 巨集,可簡寫成 F("Hello world!"),取代 const char a_string[] PROGMEM = "Hello world!";
  • ESP 內部有系統時鐘,但不包含電池關機後時鐘就停了,故每次啟動後要仰賴外部校時。最簡便做法是使用 NTPClient 程式庫向 NTP 伺服器校時,但因其未提供完整的 yyyy-MM-dd HH:mm:ss 格式時間,要自行由 getEpochTime() 換算。
  • 我在 void loop() 每秒一次讀取溫溼度(DHT22 的話要 2 秒一次),由網址 / 透過 HTML 套樣版提供即時資料,為了練習存取檔案系統,我設成每分鐘一次以 Append 方式寫入 /history.txt 檔案,另外 /history 對映到 /history.txt 請 ESPAsyncWebServer 直接從檔案系統讀檔傳回。
  • 要刪除 history.txt,最省事的做法設計一個 URL /clear 接收指令刪檔,但我嫌沒有權限管控不安全,又一次沒事找事 Orz。最後我設計了一個透過 Serial 輸入指令的機制:(這個技巧挺好用,Serial Monitor 不只用來觀察 Debug 訊息,還能反向控制開發板)
    while (Serial.available())
    {
      auto c = Serial.read();
      if (c == 13)
      {
        Serial.println();
        if (cmd == "clear")
        {
          SPIFFS.remove(historyFilePath);
          Serial.println(historyFilePath + " is deleted.");
        }
        else
        {
          Serial.println("unkown command - " + cmd);
        }
        cmd = "";
      }
      else if (c >= 32)
      {
        cmd += (char)c;
        Serial.print((char)c);
      }
    }
    

完整程式碼如下:

#include <Arduino.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <ESPAsyncWebServer.h>
#include <Guineapig.WiFiConfig.h>
#include <FS.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#ifdef ESP32 
#include <SPIFFS.h>
#define WebFS SPIFFS
#else
#include <LittleFS.h>
#define WebFS LittleFS
#define USE_LITTLE_FS
#endif


//溫溼度偵測
// https://randomnerdtutorials.com/esp32-dht11-dht22-temperature-humidity-web-server-arduino-ide/
#define DHTTYPE DHT11 // DHT 11
#ifdef ESP32
#define DHTPIN 5
#else
#define DHTPIN D3
#endif
DHT_Unified dht(DHTPIN, DHTTYPE);

//網站
AsyncWebServer server(80);

const char indexHtml[] PROGMEM = R"===(
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <style>
    dl { width: 320px; margin: 12px auto; }
    dt { 
      font-size: 20pt; color: #444; background-color: #ddd;
      margin: 6pt 0; padding: 6pt 12pt;
    }
    dd {
      text-align: right; padding-right: 6pt;
    }
    .time { font-size: 24pt; color: seagreen; }
    .num { font-size: 36pt; color: dodgerblue; }
  </style>
</head>
<body>
  <dl>
    <dt>現在時刻</dt>
    <dd class=time>%TIME%</dd>
    <dt>溫度</dt>
    <dd class=num>%TEMP% °C</dd>
    <dt>濕度</dt>
    <dd class=num>%HUMD% %</dd>
  </dl>
</body>
</html>
)===";

//網路校時
//https://randomnerdtutorials.com/esp8266-nodemcu-date-time-ntp-client-server-arduino/
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

String currTime("");
String currTemp("");
String currHumd("");

String tmplProcessor(const String &var)
{
  if (var == "TIME")
    return currTime;
  else if (var == "TEMP")
    return currTemp;
  else if (var == "HUMD")
    return currHumd;
  return "?";
}

String historyFilePath = "/history.txt";

void setup()
{
  Serial.begin(9600);
  // 啟動 DHT11 溫溼度偵測器
  dht.begin();

  // 啟動 SPIFFS/LittleFS 檔案系統
#ifdef USE_LITTLE_FS  
  LittleFS.begin();
#else
  SPIFFS.begin(true);
#endif

  if (WiFiConfig.connectWiFi())
  {
    // 使用 NTP 校時
    timeClient.begin();
    timeClient.setTimeOffset(8 * 3600); //UTC+8
    while (!timeClient.update()) {
      timeClient.forceUpdate();
    }
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
      auto *response = request->beginResponse_P(200, "text/html", indexHtml, tmplProcessor);
      request->send(response);
    });
    server.on("/history", HTTP_GET, [](AsyncWebServerRequest *request) {
      request->send(WebFS, historyFilePath, "text/plain");
    });
    server.begin();
  }
}

String cmd = "";
int lastMin = -1;

void loop()
{
  // Delay between measurements.
  delay(1000);
  unsigned long epochTime = timeClient.getEpochTime();
  struct tm *ptm = gmtime((time_t *)&epochTime);
  currTime = String(ptm->tm_year + 1900) + "-" + String(ptm->tm_mon + 1) + "-" + String(ptm->tm_mday) + " " + timeClient.getFormattedTime();
  // Get temperature event and print its value.
  sensors_event_t event;
  dht.temperature().getEvent(&event);
  currTemp = isnan(event.temperature) ? "N/A" : String(event.temperature);
  // Get humidity event and print its value.
  dht.humidity().getEvent(&event);
  currHumd = isnan(event.relative_humidity) ? "N/A" : String(event.relative_humidity);

  if (lastMin != ptm->tm_min)
  {
    lastMin = ptm->tm_min;
    auto file = WebFS.open(historyFilePath, "a");
    file.println(currTime + "," + currTemp + "," + currHumd);
    file.close();
  }

  while (Serial.available())
  {
    auto c = Serial.read();
    if (c == 13)
    {
      Serial.println();
      if (cmd == "clear")
      {
        SPIFFS.remove(historyFilePath);
        Serial.println(historyFilePath + " is deleted.");
      }
      else
      {
        Serial.println("unkown command - " + cmd);
      }
      cmd = "";
    }
    else if (c >= 32)
    {
      cmd += (char)c;
      Serial.print((char)c);
    }
  }
}

實測在 ESP8266、ESP32 都能正常執行:

我的 ESP 開發之路,又往前踏出一小步。

A simple example to measure temperature and humidity with DHT11 and ESP8266/ESP32, providing long-time recording and realtime web display.


Comments

# by ByTIM

黑大牌溫濕度計,請問哪時開賣?

# by Second

這麼棒的程式 , 讚還不按起來!!!!

Post a comment