ทำ Node32 Lite/ESP32 Dashboard ไม่ต้องง้อ M5Stack

Supphachoke Suntiwichaya
NECTEC
Published in
7 min readFeb 17, 2019

แนะนำการประยุกต์ใช้งาน web server เพื่อแสดงผลค่าจาก Sensor โดยไม่ต้องต่อ module แสดงผลเพิ่ม ในภาคแรกจะอธิบายตัวอย่างแบบไม่ต้องต่อ Sensor

Web Deashboard Node32 Lite

ตัวแสดงผลผ่านทางหน้าเว็บ หรือ Dashboard

ปกติเราต่ออุปกรณ์เสร็จก็ต้องการที่จะติดตามค่าต่าง อาจจะเป็นการต่อจอเพิ่ม หรือส่งค่าขึ้น api server ตอนแรกก็อยากหาจอมาเสียบเพิ่มเหมือนกัน แต่เมื่อศึกษาดูเค้าใช้วิธีดูผ่านหน้าเว็บจากตัว ESP32 ได้เลยซึ่งไม่ยากมาก มือใหม่แบบผมทำตามได้สบายๆ เลยอยากบันทึกไว้เผื่อมือใหม่แบบผมลองทำเล่นสนุกๆ ใช้งานได้จริงด้วย จากการทดสอบใช้สักพักก็สนุกดี บทความนี้ผมจะสาธิตการนำค่าของระบบแบบง่ายๆ ไม่กี่ค่าขึ้นมาแสดง โดยยังไม่ต้องต่อกับ sensor

อุปกรณ์

  1. Node32 Lite
  2. Client (มือถือ, Tablet หรือ Computer )

ซอร์ฟแวร์

1. Arduino IDE
2. Library ของ ESP32

วิธีการ

การใช้งาน web server เราจะใช้ความสามารถของ ESP32 ที่มี Wifi มาให้และมี library พื้นฐานมาให้แบบครบครันสามารถประยุกต์ได้ตั้งแต่เล็กๆ จนแบบซับซ้อน วิธีที่ง่ายสุดคือ การตั้งค่าให้ ESP32 เป็น Access Point แล้วใช้ Client อาจจะเป็นมือถือ Tablet หรือ คอมพิวเตอร์เชื่อมต่อเข้ามาดูค่า ซึ่งในบทความนี้จะแนะนำวิธีนี้ และ จะเขียนอีกวิธีในตอนต่อๆ ไป วิธีการเปิดเป็น Access Point เท่าที่ทดสอบอาจจะกินไฟสักหน่อย และอุณหูมิของ Ship อาจจะสูงสักหน่อยประมาณ 40 องศานิดๆ

เริ่มต้น

มาทดสอบแบบง่ายๆ กันก่อนโดยสร้าง Sketch ขึ้นมาใหม่แล้วใส่ Code ดังนี้

#include <WiFi.h>#define AppName "dashboard"const char* ssid     = AppName;
const char* password = "12345678";
void StartAP(){

Serial.printf("\nConfiguring access point...\n");

WiFi.softAP(ssid, password);
Serial.printf("AP IP address: %s\n",WiFi.softAPIP().toString().c_str());
}void setup() {
Serial.begin(115200);
StartAP();}void loop() {
// put your main code here, to run repeatedly:
}

ตัวอย่างแรกเป็นการเปิดใช้งาน WiFi ใน Mode Access Point โดยใช้ชื่อว่า dashboard และตั้งรหัสผ่าน 12345678 เมื่อ upload เรียบร้อยจะได้ Access Point ชื่อ dashboard และค่าปกติจะได้ IP 192.168.4.1

ข้อความจาก Serial monitor
รายการ Access Point บน MAC

ตอนนี้เราสามารถเชื่อมต่อไปยัง Access Point นี้ได้ละ แต่ก็ยังไม่สามารถทำอะไรได้ ต้องเพิ่ม Service Web Server มาก่อน

ข้อความ Error
16:26:06.830 -> E (603) event: mismatch or invalid event, id=63
16:26:06.830 -> E (604) event: default event handler failed!

หมายเหตุ: ระหว่างเชื่อมต่ออาจจะมีข้อความ Error ไม่ต้องตกใจนะครับยังใช้งานได้ปกติ

การเปิด Service Web Server

บน ESP32 สามารถเปิด Web Server ได้ไม่ยากและมีอยู่มากกว่าหนึ่งแบบ ผมเลือกมาแบบที่ใช้งานง่ายๆ ละกัน

เพิ่ม header

#include <WebServer.h>

ประกาศ

WebServer server(80);

สร้าง function ขึ้นมาสอง function

void handleRoot() {
server.send(200, "text/html", "Hello Client");
}
void StartWeb(){
server.on("/", handleRoot);
server.begin();

Serial.printf("HTTP server started at: http://%s\n", WiFi.softAPIP().toString().c_str());
}

handleRoot()

ใช้สำหรับส่งกลับเมื่อมีการเรียกหน้าเว็บ โดยใช้

server.send()

ส่งไป 3 ค่าคือ http status ,content-type และ content

StartWeb()

ใช้สำหรับเปิด service ของ Web Server ผมประกาศ URI หน้าแรกไว้โดยใช้

server.on()

ชี้ไปที่ / โดย callback ไปที่ handleRoot() ที่ประกาศไว้ก่อนหน้า

หลังจากนั้นก็เริ่ม start server โดยใช้

server.begin()

เพิ่มในส่วนของ setup และ loop

void setup(){  StartAP();
StartWeb();
}void loop() {

server.handleClient();
}
เชื่อมต่อ WiFi

เมื่อ upload เรียบร้อยให้เชื่อมต่อ WiFi ไปยัง Access Point dashboard แล้วเปิด web browser ไปยัง

http://192.168.4.1

เรียกไปยัง Web Server

ถ้าทุกอย่างไม่มีอะไรผิดพลาดจะได้รับข้อความกลับมาเหมือนที่ตั้งไว้ การ Setup Access Point และ Web Server บน EPS32 นั้นทำได้ไม่ยากเลยแค่นิดหน่อยก็สามารถใช้งานได้แล้ว

mDNS

ตั้งชื่อ Domain ให้ Web Server สักหน่อยจริงๆ ตอนนี้เราก็สามารถเข้าถึง web ที่เรา run ไว้ได้แล้วแต่เพื่อความเท่ ก็ตั้งชื่อให้สักหน่อยโดยใช้ความสามารถของ library mDNS หรือ multicast DNS จากที่ศึกษามีคนบอกว่าจะมีปัญหากับทางฝั่ง android แต่ผมก็ยังไม่ได้ทดสอบ แต่ windows , macOS , Linux และ iOS น่าจะใช้ได้เลย แต่เท่าที่ทดสอบมีบางจังหวะอาจจะช้ากว่าการเรียกผ่าน IP โดยตรง แต่วิธีการก็ไม่ได้ยุ่งยากอะไรทำตามขั้นตอนดังนี้

เพิ่ม header

#include <ESPmDNS.h>

สร้าง function สำหรับเริ่ม service

void StartDNS() {
if (!MDNS.begin(ssid)) {
Serial.println("Error setting up MDNS responder!");
while(1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
}

เพิ่ม function ใน setup

void setup() {
Serial.begin(115200);

StartAP();
StartWeb();
StartDNS();
}

เมื่อเพิ่มเสร็จแล้ว upload แล้วลองเชื่อมต่อ WiFi และ เรียกด้วยชื่อที่ตั้งไว้ โดยมี .local ปิดท้าย

http://dashboard.local
เรียกด้วยชื่อที่ตั้งไว้
ตัวอย่างที่ได้จาก CODE

ปรับแต่งหน้าเว็บ

เมื่อเราเข้าใจหลักการพื้นฐานแล้วก็สามารถนำมาประยุกต์ได้หลากหลาย เดี๋ยวผมจะแนะนำการปรับแต่งหน้าตาเว็บเพิ่มเติมเพื่อให้ดูเป็น Dashboard บ้าง ฮาๆ

การ serve html ถ้าไม่เยอะเราสามารถฝังใน code ได้เลย แต่ถ้าเยอะอาจจะใช้เทคนิคการเก็บไว้ใน SPIFFS จะสะดวกกว่า ซึ่งเดี๋ยวจะหาเวลากล่าวถึงอีกในตอนต่อไป โดยจะแนะนำเทคนิคเอา framework ใหญ่ๆ อย่าง bootstrap หรือ veutify มาเล่นกันบน ESP32 (ถ้าทดลองแล้วได้ผลดีนะ ถ้าไม่เวิร์คค่อยว่ากัน ฮาๆ)

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

  1. / → index
  2. /style.css → cascading style sheet
  3. /index.js → javascript
  4. /info → json ของข้อมูล
  5. /onoff → ส่ง เปิดปิด LED

เทคนิคการเก็บ HTML ในบทความนี้จะใช้วิธีเก็บไว้แบบ PROGMEM คือจะเก็บไว้ในส่วนของ flash

// api ดึงอุณหูมิ CPU
#ifdef __cplusplus
extern "C" {
#endif
uint8_t temprature_sens_read();
#ifdef __cplusplus
}
#endif
uint8_t temprature_sens_read();
// ส่งค่าอุณหภูมิเป็นองศาเซสเซียส
String CpuTemp(){
int temp = ((temprature_sens_read() - 32) / 1.8);return String(temp);
}
// ส่งสถานะ LED
String LedStatus(){
return ( digitalRead(LED_BUILTIN) == LOW) ? "true":"false";
}
// HTML ของหน้าแรก
void HandleRoot() {
String s PROGMEM = R"=====(<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="style.css"> <title>ESP32:Dashboard</title></head><body> <div id="container"> <header class="header"> <div><h2>Dashboard</h2></div></header> <main class="main"> <div class="main-overview"> <div class="overviewcard"> <div class="overviewcard__icon">CPU Temp</div><div class="overviewcard__info"><span id="temp">N/A</span> <sup>o</sup>C</div></div><div class="overviewcard"> <div class="overviewcard__icon">LED</div><div class="overviewcard__info"> <input type="checkbox" id="led" onclick="onoff(this)"> </div></div><div class="overviewcard"> <div class="overviewcard__icon">IP</div><div class="overviewcard__info" id="ip">N/A</div></div><div class="overviewcard"> <div class="overviewcard__icon">MAC</div><div class="overviewcard__info" id="mac">N/A</div></div><div class="overviewcard"> <div class="overviewcard__icon">MEM</div><div class="overviewcard__info" id="mem">N/A</div></div></div></main> <footer class="footer"> <div class="footer__copyright">&copy; 2019 </div><div class="footer__signature">ESP32 WebServer Library</div></footer> </div></div><script src="index.js"></script></body></html>)=====";

server.send(200, "text/html", s);
}// CSS
void HandleCss() {
String css PROGMEM = R"=====( html,body{margin: 0;height: 100%;color: #fff;}#container{display: flex;flex-direction: column;}.header{align-items: center;justify-content: space-between;padding-top: 10px 15px;background-color: #648ca6;width: 100%;}.header h2{margin-left: 15px;}.main{background-color: #8fd4d9;padding-top: 20px;overflow: auto;flex: 1;min-width: 100vw;min-height: 100vh;}.main-header{display: flex;justify-content: space-between;margin: 20px;padding: 20px;height: 50px;background-color: #e3e4e6;color: slategray;}.main-overview{display: grid;grid-template-columns: repeat(auto-fit, minmax(265px, 1fr));grid-auto-rows: 94px;grid-gap: 20px;margin: 20px;}.overviewcard{display: flex;align-items: center;justify-content: space-between;padding: 20px;background-color: #d3d3;}.main-cards{column-count: 1;column-gap: 20px;margin: 20px;}.card{display: flex;flex-direction: column;align-items: center;width: 100%;background-color: #82bef6;margin-bottom: 20px;-webkit-column-break-inside: avoid;padding: 24px;box-sizing: border-box;}.card:first-child{height: 485px;}.card:nth-child(2){height: 200px;}.card:nth-child(3){height: 265px;}.footer{grid-area: footer;display: flex;align-items: center;justify-content: space-between;padding: 0 16px;background-color: #648ca6;} )=====";

server.send(200, "text/css", css);
}// JavaScriptvoid HandleJs() {
String js PROGMEM = R"=====(function onoff(e){var n=new XMLHttpRequest;n.open("POST","onoff",!1),n.send(e.checked),n.response?alert(n.response):alert("UnSuccess!!")}function getData(e){var n=new XMLHttpRequest;n.onreadystatechange=function(){if(4==this.readyState&&200==this.status)for(var e=JSON.parse(this.responseText),n=0;n<e.length;n++)j=e[n],document.getElementById(j.name)&&("led"===j.name?document.getElementById(j.name).checked=j.val:document.getElementById(j.name).innerHTML=j.val),document.getElementById("label_"+j.name)&&(document.getElementById("label_"+j.name).innerHTML=j.name)},n.open("GET",e,!0),n.send()}getData("info"),setInterval(function(){getData("info")},5e3);)=====";

server.send(200, "application/javascript", js);
}// รับ POST จาก Client แล้วสั่งเปิด ปิด LED
void HandleOnOff(){
uint8_t cmd = (server.arg(0) == "true") ? LOW : HIGH;
String led = (cmd == LOW) ? "ON":"OFF";

digitalWrite(LED_BUILTIN, cmd);
Serial.println(server.arg(0));

server.send(200, "text/plain", led);
}
// ส่งค่าต่างๆ ที่ต้องการแบบ JSON
void HandleInfo(){
String data = "[";
data += "{\"name\": \"temp\",\"val\":" + CpuTemp() + "}";
data += ",{\"name\":\"led\",\"val\":" + LedStatus() +"}";
data += ",{\"name\":\"mac\",\"val\":\"" + String(WiFi.macAddress())+"\"}";
data += ",{\"name\":\"ip\",\"val\":\"" + (WiFi.softAPIP()).toString() +"\"}";
data += ",{\"name\":\"mem\",\"val\":\"" + String(esp_get_free_heap_size()/1024) +" KB\"}";

data += "]";

server.send(200, "application/json", data);
}

ข้อมูลที่ผมจะส่งไปจะมีดังนี้

  1. อุณหภูมิของ CPU
  2. สถานะของ BUILT IN LED
  3. IP ของ Access Point
  4. MAC Address ของ อุปกรณ์
  5. Free Heap Mem

สังเกตว่าตัว HTML, CSS และ JS ผมจะ minify ก่อนตัวอย่างข้อมูลยังไม่เยอะสามารถใช้วิธีนี้ได้อยู่ แต่ถ้า file ใหญ่ๆ นี่ถ้ามีโอกาสก็จะเล่าให้ฟัง

หน้าแรก

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><link rel="stylesheet" href="style.css"><title>ESP32:Dashboard</title></head><body><div id="container"><header class="header"><div><h2>Dashboard</h2></div></header><main class="main"><div class="main-overview"><div class="overviewcard"><div class="overviewcard__icon">CPU Temp</div><div class="overviewcard__info"><span id="temp">N/A</span> <sup>o</sup>C</div></div><div class="overviewcard"><div class="overviewcard__icon">LED</div><div class="overviewcard__info"><input type="checkbox" id="led" onclick="onoff(this)"></div></div><div class="overviewcard"><div class="overviewcard__icon">IP</div><div class="overviewcard__info" id="ip">N/A</div></div><div class="overviewcard"><div class="overviewcard__icon">MAC</div><div class="overviewcard__info" id="mac">N/A</div></div><div class="overviewcard"><div class="overviewcard__icon">MEM</div><div class="overviewcard__info" id="mem">N/A</div></div></div></main><footer class="footer"><div class="footer__copyright">&copy; 2019 </div><div class="footer__signature">ESP32 WebServer Library</div></footer></div></div><script src="index.js"></script></body></html>

/style.css

html,body {margin: 0;height: 100%;color: #fff;}#container {display: flex;flex-direction: column;}.header {align-items: center;justify-content: space-between;padding-top: 10px 15px;background-color: #648ca6;width: 100%;}.header h2 {margin-left: 15px;}.main {background-color: #8fd4d9;padding-top: 20px;overflow: auto;flex: 1;min-width: 100vw;min-height: 100vh;}.main-header {display: flex;justify-content: space-between;margin: 20px;padding: 20px;height: 50px;background-color: #e3e4e6;color: slategray;}.main-overview {display: grid;grid-template-columns: repeat(auto-fit, minmax(265px, 1fr));grid-auto-rows: 94px;grid-gap: 20px;margin: 20px;}.overviewcard {display: flex;align-items: center;justify-content: space-between;padding: 20px;background-color: #d3d3;}.main-cards {column-count: 1;column-gap: 20px;margin: 20px;}.card {display: flex;flex-direction: column;align-items: center;width: 100%;background-color: #82bef6;margin-bottom: 20px;-webkit-column-break-inside: avoid;padding: 24px;box-sizing: border-box;}.card:first-child {height: 485px;}.card:nth-child(2) {height: 200px;}.card:nth-child(3) {height: 265px;}.footer {grid-area: footer;display: flex;align-items: center;justify-content: space-between;padding: 0 16px;background-color: #648ca6;}

ตัวอย่างผมเอามาจาก https://codepen.io/trooperandz/pen/YRpKjo

การทำ minify ผมใช้

/index.js

getData('info');setInterval(function() {getData('info');}, 5000);function onoff(el) {var request = new XMLHttpRequest();request.open('POST', 'onoff', false);request.send(el.checked);if (request.response) alert(request.response);else alert('UnSuccess!!');}function getData(uri) {var xhttp = new XMLHttpRequest();xhttp.onreadystatechange = function() {if (this.readyState == 4 && this.status == 200) {var jj = JSON.parse(this.responseText);for (var i = 0; i < jj.length; i++) {j = jj[i];if (document.getElementById(j.name)) {j.name === 'led'? (document.getElementById(j.name).checked = j.val): (document.getElementById(j.name).innerHTML = j.val);}if (document.getElementById('label_' + j.name)) {document.getElementById('label_' + j.name).innerHTML = j.name;}}}};xhttp.open('GET', uri, true);xhttp.send();}

ในส่วนของ JavaScript จะเป็นตัวหลักในการควบคุมการแสดงผลซึ่งจะใช้ JavaScript Pure จะดึงข้อมูลทุกๆ 5 วินาที

การดึงข้อมูลจะใช้ GET ส่วน การสั่ง ON/OFF ผมจะใช้ POST

ให้เพิ่มใน function StartWeb

void StartWeb(){
server.on("/", HandleRoot);
server.on("/style.css", HandleCss);
server.on("/index.js", HandleJs);
server.on("/onoff", HandleOnOff);
server.on("/info", HandleInfo);
server.begin();
Serial.printf("HTTP server started at: \n\t - http://%s\n\t - http://%s.local\n",WiFi.softAPIP().toString().c_str(),WiFi.softAPgetHostname());
}

และ เพิ่มใน setup สำหรับกำหนด LED

void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
StartAP();
StartWeb();
StartDNS();
}

แค่นี้ก็ได้ Dashboard แบบขี้เหร่ๆ มาละ ซึ่งถ้าได้หลักการก็สามารถประยุกต์ใช้แสดงค่าจาก Sensor อะไรก็ได้ละ แถมสามารถควบคุมอุปกรณ์ได้ด้วยไม่ว่าจะเป็น motor หรือ สวิตช์ต่างๆ

สามารถเอา code ไปลองเล่นดูได้ครับ

git clone https://github.com/mrchoke/ESP32_Dashboard_APMode

--

--