[TH] ESP8266 WebServer

บทความนี้เป็นการทดลองทำให้ไมโครคอนโทรลเลอร์ esp8266 เป็นเครื่องให้บริการเว็บเพื่อแสดงผลค่าอุณหภูมิและความชื้นจากเซ็นเซอร์ DHT11 โดยใช้ไลบรารีของ Adafruit ดังภาพที่ 1 และเมื่อกำหนดให้ไมโครคอรโทรลเลอร์ทำงานในโหมด SoftAP เพื่อให้ลูกข่ายหรือผู้ใช้เชื่อมต่อ WiFi เข้ามาหลังจากนั้นใช้ Browser เข้าไปยัง IP หมายเลข 192.168.4.1 ซึ่งเป็นหมายเลขของ esp8266

ภาพที่ 1 ตัวอย่างการการต่ออุปกรณ์สำหรับบทความ

อุปกรณ์

จากภาพที่ 1 จะพบว่าทีมงานเราใช้รายการอุปกรณ์ดังนี้

  1. NodeMCU
  2. NodeMCU Base
  3. DHT11 Module

การเชื่อมต่อ

ภาพที่ 2 การต่อวงจร

จากวงจรในภาพที่ 2 จะพบว่าทางทีมงานเราเลือกใช้ขา GPIO05 หรือ D1 ของไมโครคอนโทรลเลอร์ esp8266 และเลือกใช้โมดูล DHT11 ที่มีการต่อ R เอาไว้ในตัวดังภาพที่ 4 ดังนั้นให้เชื่อมขาตามภาพที่ 3 และ 4 คือ

  • เส้นสีเหลืองต่อเข้ากับ 3V3 และ +
  • เส้นสีน้ำตาลต่อเข้ากับ GND และ –
  • เส้นสีส้มต่อเข้ากับ D1 หรือ GPIO05 และ s
ภาพที่ 3 การต่อที่ฝั่ง esp8266
ภาพที่ 4 การเชื่อมต่อที่ DHT11

ทดสอบโปรแกรมเพื่อให้แน่ใจว่าทำงานได้ถูกต้องด้วยโค้ดต่อไปนี้ และตัวอย่างของผลลัพธ์เป็นดังภาพที่ 5

#include <DHT.h>

DHT dht = DHT(5, DHT11); // D1
float minC = 100.0f, maxC = 0.0f; // อุณหภูมิต่ำสุด/สูงสุด
float minH = 100.0f, maxH = 0.0f; // ความชื้นต่ำสุด/สูงสุด
void setup() {
  Serial.begin(115200);
  Serial.println("\n\n\n");
  dht.begin();
}

void loop() {
  float h = dht.readHumidity();
  float tc = dht.readTemperature();
  float tf = dht.readTemperature(true);

  if (isnan(h) || isnan(tc) || isnan(tf)) {
    Serial.println("DHT11 connect failed!");
    return;
  }

  float hic = dht.computeHeatIndex(tc, h, false);
  float hif = dht.computeHeatIndex(tf, h);

  if (minC > tc) {
    minC = tc;
  }
  if (maxC < tc) {
    maxC = tc;
  }
  if (minH > h) {
    minH = h;
  }
  if (maxH < h) {
    maxH = h;
  }

  Serial.printf("Temperature: %.2fC/%.2fF Huminitt: %.2f%%, Heat index: %.2fC/%.2fF\n",
                tc, tf, h, hic, hif);
  Serial.printf("Temp. (%.2fC-%.2fC) Hum. (%.2f%%-%.2f%%)\n",
                minC, maxC, minH, maxH);
  delay(10000);
}
ภาพที่ 5 ตัวอย่างผลลัพธ์ของการอ่านค่าจากเซ็นเซอร์ dht11

ตัวอย่างโปรแกรม

จากตัวอย่างการทดสอบการทำงานของวงจรเชื่อมต่อกับเซ็นเซอร์ DHT11 ให้กลายเป็น Server จะได้ว่าทางเราได้สร้างตัวแปรภายนอกสำหรับเก็บค่าต่าง ๆ ดังต่อไปนี้

float minC = 100.0f, maxC = 0.0f; // อุณหภูมิต่ำสุด/สูงสุด
float minH = 100.0f, maxH = 0.0f; // ความชื้นต่ำสุด/สูงสุด
float hic; // headt index ในหน่วย C
float hif;// headt index ในหน่วย F
float h; // ค่าความชื้น
float tc; // ค่าอุณหภูมิในหน่วย C
float tf; // ค่าอุณหภูมิในหน่วย F

นอกจากนี้ได้แยกส่วนของการอ่านค่าเอาไว้ในฟังก์ชันสำหรับอ่านค่าชื่อ getDHT11() ดังนี้ ซึ่งได้มีการตรวจสอบกรณีที่เซ็นเซอร์ไม่ทำงานด้วยการให้ค่าเป็น -1.0 โดยไม่คำนวณต่า min/max ของอุณหภูมิและความชื้นใหม่

void getDHT11() {
  h = dht.readHumidity();
  tc = dht.readTemperature();
  tf = dht.readTemperature(true);

  if (isnan(h) || isnan(tc) || isnan(tf)) {
    h = -1.0f;
    tc = -1.0f;
    tf = -1.0f;
    hic = -1.0f;
    hif = -1.0f;
    return;
  }

  hic = dht.computeHeatIndex(tc, h, false);
  hif = dht.computeHeatIndex(tf, h);

  if (minC > tc) {
    minC = tc;
  }
  if (maxC < tc) {
    maxC = tc;
  }
  if (minH > h) {
    minH = h;
  }
  if (maxH < h) {
    maxH = h;
  }
}

โดยหลักการทำงานของ HTTP จะทำการส่งข้อความร้องขอชุดแรกเป็นรูปแบบดังนี้มาที่พอร์ตหมายเลข 80 (สำหรับกรณีที่เป็น http)

GET ทรัพยากรที่ร้องขอ HTTP/1.1

เช่น GET / HTTP/1.1 หมายถึงร้องขอเรียกหน้าเว็บหลัก เราจึงเขียนฟังก์ชันสำหรับทำหน้าที่ตอบสนองหน้าเว็บหลักที่เรียก getDHT11() เพื่ออ่านค่าปัจจุบันของอุณหภูมิความชื้นพร้อมทั้งคำนวณ Heat Index กับค่าต่ำสุด/สูงสุดของอุณหภูมิแบบองศาเซลเซียสและความชื้นสัมพัทธ์ หลังจากนั้นทำการตอบกลับไปยังเครื่องลูกข่ายที่ร้องขอเข้ามาโดยส่งรหัส HTTP/1.1 200 OK เป็นข้อมูลชุดแรกซึ่งรหัส 200 เป็นรหัสตอบกลับให้ทราบว่าการร้องขอของลูกข่ายนั้นประสบความสำเร็จ โดยรหัสตอบกลับอื่น ๆ ได้แก่

  • 200 การร้องขอประสบความสำเร็จ และได้ตอบกลับข้อมูลกลับมา
  • 404 ไม่พบทรัพยากรที่ร้องขอ
  • 302 สิ่งที่ร้องขอนั้นย้ายที่อยู่ไปแล้ว
  • 500 เครื่องให้บริการไม่พร้อมให้บริการในสิ่งที่ร้องขอ
String htmlPage() {
  String html;
  getDHT11();
  html.reserve(2048);               // prevent ram fragmentation
  html = F("HTTP/1.1 200 OK\r\n"
           "Content-Type: text/html\r\n"
           "Connection: close\r\n"  // the connection will be closed after completion of the response
           "Refresh: 5\r\n"         // refresh the page automatically every 5 sec
           "\r\n"
           "<!DOCTYPE HTML>"
           "<html><head></head><body>"
           "<h1>DHT11</h1>");
  html += F("<div>Temperature:");
  html += tc;
  html += F("C/");
  html += tf;
  html += F("F</div>");
  html += F("<div>Huminity:");
  html += h;
  html += F("%</div>");
  html += F("<div>Temperature:");
  html += minC;
  html += F("C-");
  html += maxC;
  html += F("C</div>");
  html += F("<div>Huminity:");
  html += minH;
  html += F("%-");
  html += maxH;
  html += F("%</div>");
  html += F("</body></html>\r\n");
  return html;
}

ดังนั้น เมื่อนำทั้งหมดไปรวมกับตัวอย่างเรื่องของ WiFiServer เข้ากับการตอบสนองเป็นหน้าเว็บเพจรายงานค่าที่ได้จากเซนเซอร์จึงเขียนได้ดังนี้

#include <DHT.h>
#include <ESP8266WiFi.h>

#define AP_NAME "JarutEx"
#define AP_PASSWD "123456789"
IPAddress myIP(192, 168, 4, 1);
IPAddress gwIP(192, 168, 4, 10);
IPAddress subnet(255, 255, 255, 0);

DHT dht = DHT(5, DHT11); // D1
WiFiServer server(80);

float minC = 100.0f, maxC = 0.0f; // อุณหภูมิต่ำสุด/สูงสุด
float minH = 100.0f, maxH = 0.0f; // ความชื้นต่ำสุด/สูงสุด
float hic; // headt index ในหน่วย C
float hif;// headt index ในหน่วย F
float h; // ค่าความชื้น
float tc; // ค่าอุณหภูมิในหน่วย C
float tf; // ค่าอุณหภูมิในหน่วย F

void getDHT11() {
  h = dht.readHumidity();
  tc = dht.readTemperature();
  tf = dht.readTemperature(true);

  if (isnan(h) || isnan(tc) || isnan(tf)) {
    h = -1.0f;
    tc = -1.0f;
    tf = -1.0f;
    hic = -1.0f;
    hif = -1.0f;
    return;
  }

  hic = dht.computeHeatIndex(tc, h, false);
  hif = dht.computeHeatIndex(tf, h);

  if (minC > tc) {
    minC = tc;
  }
  if (maxC < tc) {
    maxC = tc;
  }
  if (minH > h) {
    minH = h;
  }
  if (maxH < h) {
    maxH = h;
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("\n\n\n");
  dht.begin();
  if (WiFi.softAPConfig( myIP, gwIP, subnet )) {
    if (WiFi.softAP(AP_NAME, AP_PASSWD, 8, false, 5)) {
      Serial.print("IP Address : ");
      Serial.println(WiFi.softAPIP());
    } else {
      Serial.println("softAP() failed!!");
      while (true);
    }
  } else {
    Serial.println("softAPConfig() failed!");
    while (true);
  }
  server.begin();
}

String htmlPage() {
  String html;
  getDHT11();
  html.reserve(2048);               // prevent ram fragmentation
  html = F("HTTP/1.1 200 OK\r\n"
           "Content-Type: text/html\r\n"
           "Connection: close\r\n"  // the connection will be closed after completion of the response
           "Refresh: 5\r\n"         // refresh the page automatically every 5 sec
           "\r\n"
           "<!DOCTYPE HTML>"
           "<html><head></head><body>"
           "<h1>DHT11</h1>");
  html += F("<div>Temperature:");
  html += tc;
  html += F("C/");
  html += tf;
  html += F("F</div>");
  html += F("<div>Huminity:");
  html += h;
  html += F("%</div>");
  html += F("<div>Temperature:");
  html += minC;
  html += F("C-");
  html += maxC;
  html += F("C</div>");
  html += F("<div>Huminity:");
  html += minH;
  html += F("%-");
  html += maxH;
  html += F("%</div>");
  html += F("</body></html>\r\n");
  return html;
}

void loop() {
  WiFiClient client = server.available();

  if (client)   {
    Serial.println("\n[Client connected]");
    while (client.connected()) {
      if (client.available()) {
        String req = client.readStringUntil('\r');
        Serial.print(req);
        if (req.indexOf("GET / HTTP/1.1")) {
          client.println(htmlPage());
          break;
        }
      }
    }

    while (client.available()) {
      client.read();
    }

    client.stop();
    Serial.println("[Client disconnected]");
  }
}

ตัวอย่างผลลัพธ์การทำงานเป็นดังภาพที่ 6

ภาพที่ 6 ตัวอย่างผลลัพธ์จากโค้ด WebServer

คลาส ESP8266WebServer

จากตัวอย่างการทำต้วเองเป็นเครื่องให้บริการเว็บจะพบว่า การเขียนโปรแกรมจะต้องคอยตรวจสอบข้อความที่เข้ามาและทำการคัดแยกเพื่อตีความสิ่งที่ได้รับ เช่น เมื่อพบ “GET / HTTP/1.1” หมายถึงลูกข่ายร้องขอเข้าถึงไดเร็กทอรีรากหรือหน้าเว็บหลัก ถ้าเป็นการร้องขอหาเว็บอื่นเช่น index9.html จะกลายเป็น “GET /index9.html HTTP/1.1” ดังนั้น ผู้เขียนโปรแกรมจะต้องเพิ่มส่วนของการแยกคำเพื่อให้มั่นใจว่าลูกข่ายร้องขอทรัพยากรใด ด้วยเหตุนี้จึงมีคลาส ESP8266WebServer ที่ช่วยให้การเขียนโปรแกรมเพื่อทำหน้าที่ให้บริการเว็บสะดวกยิ่งขึ้น

จากตัวอย่างก่อนหน้านี้เมื่อเขียนด้วย ESP8266WebServer จะได้โค้ดดังนี้

#include <DHT.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

#define AP_NAME "JarutEx"
#define AP_PASSWD "123456789"
IPAddress myIP(192, 168, 4, 1);
IPAddress gwIP(192, 168, 4, 10);
IPAddress subnet(255, 255, 255, 0);

DHT dht = DHT(2, DHT11);
ESP8266WebServer server(80);


float minC = 100.0f, maxC = 0.0f; // อุณหภูมิต่ำสุด/สูงสุด
float minH = 100.0f, maxH = 0.0f; // ความชื้นต่ำสุด/สูงสุด
float hic; // headt index ในหน่วย C
float hif;// headt index ในหน่วย F
float h; // ค่าความชื้น
float tc; // ค่าอุณหภูมิในหน่วย C
float tf; // ค่าอุณหภูมิในหน่วย F

void getDHT11() {
  h = dht.readHumidity();
  tc = dht.readTemperature();
  tf = dht.readTemperature(true);

  if (isnan(h) || isnan(tc) || isnan(tf)) {
    h = -1.0f;
    tc = -1.0f;
    tf = -1.0f;
    hic = -1.0f;
    hif = -1.0f;
    return;
  }

  hic = dht.computeHeatIndex(tc, h, false);
  hif = dht.computeHeatIndex(tf, h);

  if (minC > tc) {
    minC = tc;
  }
  if (maxC < tc) {
    maxC = tc;
  }
  if (minH > h) {
    minH = h;
  }
  if (maxH < h) {
    maxH = h;
  }
}


void setup() {
dht.begin();
Serial.begin(115200);
Serial.println("\n\r\n\r");
  if (WiFi.softAPConfig( myIP, gwIP, subnet )) {
    if (WiFi.softAP(AP_NAME, AP_PASSWD, 8, false, 5)) {
      Serial.print("IP Address : ");
      Serial.println(WiFi.softAPIP());
    } else {
      while (true);
    }
  } else {
    while (true);
  }
  server.on("/", htmlPage);
  server.begin();
}

void htmlPage() {
  String html;
  getDHT11();
  html.reserve(2048);               // prevent ram fragmentation
  html = F(
           "<!DOCTYPE HTML>"
           "<html><head>"
           "<meta name='viewport' content='width=device-width, initial-scale=1'>"
           "<style>"
           "html {font-family: Arial; display: inline-block; text-align: center;}"
           "h1 {font-size: 3.0rem;}"
           "p {font-size: 3.0rem;}"
           "body {max-width: 800px; margin:0px auto; padding-bottom: 16px;}"
           "</style>"
           "</head><body>"
           "<h1>DHT11</h1>"
         );

  html += F("<div>Temperature:");
  html += tc;
  html += F("C/");
  html += tf;
  html += F("F</div>");
  html += F("<div>Huminity:");
  html += h;
  html += F("%</div>");
  html += F("<div>Temperature:");
  html += minC;
  html += F("C-");
  html += maxC;
  html += F("C</div>");
  html += F("<div>Huminity:");
  html += minH;
  html += F("%-");
  html += maxH;
  html += F("%</div>");
  html += F("</body></html>\r\n");
  server.send(200, "text/html", html);
}

void loop() {
  server.handleClient();
}

จากตัวอย่างจะได้ว่าทางทีมงานเราได้ปรับแก้ส่วนของ HTML เพิ่มเติมในเรื่องการควบคุมการแสดงผลใน CSS เพื่อให้ตัวอักษรแสดงได้เหมาะสมกับหลายอุปกรณ์มากขึ้น ในส่วนของการใช้ ESP8266WebServer ถูกนำมาใช้แทน WiFiServer และเหลือการทำงานเพียง 3 ส่วนหลัก คือ

  1. การตั้งค่าการดักการร้องขอ และการตอบกลับ
    1. ด้วยการใช้ server.on( ทรัพยากร, ฟังก์ชั้นตอบกลับ )
    2. ในฟังก์ชันตอบกลับเรียก server.send( รหัสตอบกลับ, ประเภทข้อมูล, ข้อมูล )
  2. การเริ่มต้นทำงาน ด้วยการเรียก server.begin() ใน setup()
  3. การรอการร้องขอจากลูกข่ายด้วยการเรียก server.handleClient() ใน loop()

สรุป

จากบทความนี้จะพบว่าถ้าเราเข้าใจหลักการสื่อสารของโพรโทคอลต่าง ผู้เขียนโปรแกรมสามารถใช้ไมโครคอนโทรลเลอร์ esp8266 เป็นตัวให้บริการหรือลูกข่ายเพื่อสื่อสารการทำงานกับโพรโทคอลนั้นได้ อย่างในตัวอย่างครั้งนี้เป็นการใช้เรื่องของ WiFiServer ของ esp8266 ดักการทำงานที่พอร์ต 80 ซึ่งเป็นพอร์ตของโพรโทคอล HTTP และเมื่อพบข้อความที่ร้องขอทรัพยากรเราจึงตอบกลับเป็นรูปแบบที่ HTTP เข้าใจ ผลลัพธ์ที่ออกมาทำให้แสดงผลบนเว็บบราวเซอร์ได้ถูกต้องดังภาพที่ 6 สุดท้ายนี้ขอให้สนุกกับการเขียนโปรแกรมครับ

ท่านใดต้องการพูดคุยสามารถคอมเมนท์ไว้ได้เลยครับ

แหล่งอ้างอิง

  1. ESP8266 Arduino Core :Server

(C) 2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-08-01, 2021-08-08, 2021-08-10, 2021-11-08