ใส่ใจ 9. การพัฒนาโปรแกรมควบคุมใส่ใจด้วย ESP32 ตอนที่ 2

แบ่ง Task การทำงานของเป็นส่วนๆ หน้าที่ใครหน้าที่มัน

เรียกได้ว่าจัดเต็มให้ ESP32 ทำงานอย่างเต็มที่ มีการเชื่อมต่อเครือข่ายไร้สาย WiFi ไปยัง Blynk Server แจ้งเตือนไป LINE NOTIFY ส่งข้อมูลไปเก็บที่ฐานข้อมูลออนไลน์ Google Firebase และแยกการทำงานแบบ Multitask ด้วย FreeTROS ทุกอย่างเบ็ดเสร็จเด็จขาดในตัวมันเอง สุดๆ เลยครับแบบนี้

การทำงานแบบนี้ถ้าเป็นสมัยก่อนมันก็คือเครื่องคอมพิวเตอร์ดีๆ ตัวหนึ่งเลย ไม่น่าเชื่อว่าวันนี้ ESP32 ที่เป็นอุปกรณ์สมองกลขนาดเล็กก็สามารถทำได้ขนาดนี้ ค่าตัวมันก็ไม่แพงด้วยแค่หลักร้อยเท่านั้นเอง ยอดเยี่ยมมากครับ

ก่อนที่จะดูโค๊ดกันต่อ มาทำความเข้าใจเกี่ยวกับ “การนับเวลา” ใน ESP32 กันก่อน ทุกครั้งที่บอร์ดเร่ิมทำงาน EP32 ก็จะเร่ิมนับเวลาทันที เวลาที่นับมีหน่วยเป็นมิลลิวินาที มันจะนับเวลาไปเรื่อยๆ จนถึงประมาณ 50 วันก็จะมาเร่ิมนับ 0 ใหม่ แต่ถ้ามีการรีบูตเมื่อไหร่เวลาก็จะกลับมาเป็น 0 ใหม่ทันที เวลาที่พูดถึงนี้ไม่ใช่เวลาที่อ่านได้แบบนาฬิกาหรูนะครับ มันบอกไม่ได้ว่าเป็นวันไหน เดือนไหน ปีไหน ชั่วโมง นาที เท่าไหร่ มันไม่รู้หรอก มันแค่นับไปเรื่อยๆ ทุกๆ มิลลิวินาที

ตัวแปรที่เก็บค่าเวลานี้ จะกำหนดเป็น unsigned long เก็บค่า 32 บิต (4 ไบต์) และไม่เก็บจำนวนที่เป็นลบ มีขอบเขตตั้งแต่ 0 ถึง 4,294,967,295 ลองมาดูตัวอย่างการเขียนโค๊ดนำเวลามาใช้ด้วยฟังก์ชัน millis()

unsigned long time;

void setup(){
Serial.begin(115200);
}
void loop(){
Serial.print("Time: ");
time = millis();

Serial.println(time);
delay(1000);
}

จากโค๊ดข้างบนจะกำหนดให้ตัวแปร time เป็นแบบ unsigned long จากนั้นจะทำการอ่านค่าเวลาด้วยฟังก์ชัน millis() แล้วนำเวลาที่ได้มาแสดงใน Serial Monitor รอ 1 วินาทีและทำซ้ำใหม่

การที่ ESP32 นับเวลาเป็นจังหวะที่สม่ำเสมอแบบนี้ เราก็จะนำมาใช้เป็นประโยชน์ได้หลายอย่างเลยครับ ถ้าบอกว่าให้ ESP32 ตั้งเวลา เช่น อีก 1 นาทีเร่ิมทำอะไรสักอย่าง หรือถ้าบอกว่าทุกๆ 5 นาทีสั่งให้ฟังก์ชันทำงาน แบบนี้ทำได้สบายเลยครับ

มาเข้าเรื่องอธิบายการเขียนโค๊ดกันต่อครับ จากตอนที่ผ่านมาเป็นการประกาศตัวแปรใช้งานต่างๆ การตั้งค่าการทำงานของระบบ และฟังก์ชันการทำงานหลัก ในตอนนี้จะเป็นการสร้างฟังก์ขันขึ้นมาใช้งานเอง และการสร้างทาสก์ทำงานจำนวน 5 ทาสก์

(15) CHECK CONNECTION

จุดเล็กจุดน้อยเราก็ไม่ควรมองข้าม ทุกสิ่งทุกอย่างย่อมเกิดขึ้นได้เสมอควรป้องกันไว้ก่อน ฟังก์ชันนี้จะคอยเช็คการเชื่อมต่อระหว่างบอร์ด ESP32 ไปยัง Blynk Server อย่างสม่ำเสมอตามระยะเวลาที่กำหนด ถ้าเกิดเหตุการณ์ Blynk Server ล่มก็จะทำการเฝ้าติดตามดู และทันทีที่ Blynk Server กลับมาทำงาน ก็จะทำการเชื่อมต่อให้เองแบบอัตโนมัติ มีตัวช่วยแบบนี้ค่อยอุ่นใจกันหน่อย วางใจได้ว่าระบบทำงานได้อย่างต่อเนื่อง

// *** (15) CHECK CONNECTION ***//เช็คการเชือมต่อของ blynk
void CheckConnection(){
//ถ้าไม่สามารถ connect ไปยัง blynk server ได้
if(!Blynk.connected()){
//แสดงข้อความบนคอนโซล
Serial.println("Not connected to Blynk server.");
Serial.println("Connecting to Blynk server...");
Blynk.connect(); // try to connect to server }
}

16) SEND LINENOTIFY

ฟังก์ชันนี้จะทำหน้าที่ส่งข้อความการแจ้งเตือนไปยัง LINE NOTIFY โดยใช้การเชื่อมต่อแบบปลอดภัย (https) ไปยัง notify-api.line.me ผ่านพอร์ต 443 ทุกครั้งที่ส่งการแจ้งเตือนจะแสดง Log ลงใน Serial Monitor ด้วย

// *** (16) SEND LINENOTIFY ***void NotifyLine(String t) {WiFiClientSecure client;  if(!client.connect("notify-api.line.me", 443)) {
Serial.println("Connection failed");
return;
}
String req = "";
req += "POST /api/notify HTTP/1.1\r\n";
req += "Host: notify-api.line.me\r\n";
req += "Authorization: Bearer " + String(TokenLine) + "\r\n";
req += "Cache-Control: no-cache\r\n";
req += "User-Agent: ESP32\r\n";
req += "Content-Type: application/x-www-form-urlencoded\r\n";
req += "Content-Length: " + String(String("message=" + t).length()) + "\r\n";
req += "\r\n";
req += "message=" + t;
Serial.println(req);
client.print(req);
vTaskDelay(20 / portTICK_PERIOD_MS);Serial.println("-------------"); while(client.connected()) { String line = client.readStringUntil('\n'); if(line == "\r") { break;
}
}
}

17) CURRENT DATETIME

เนื่องจาก ESP32 ไม่มี Module Clock ในตัวเอง แน่นอนว่ามันไม่มีนาฬิกาหรูบอกเวลาแน่ๆ แล้วถ้าต้องการใช้เวลามาเก็บ log ต้องทำอย่างไร ? พระเอกงานนี้ก็ตกไปเป็นของ NTP คือดึงเวลาจาก Time Server ผ่านอินเตอร์เน็ตมาใช้งานนั่นเอง มาหมดเลยทั้งวัน เดือน ปี และเวลา

แต่เท่าที่ผู้เขียนได้ลองทดสอบการดึงเวลามาใช้งาน พบว่าบางครั้งเจอวันเวลาที่ผิดพลาดอยู่เหมือนกัน ปีที่ได้มันจะเป็นปี 1970 ดังนั้นผู้เขียนจึงได้สร้างเงื่อนไขให้มันพยายามดึงเวลาใหม่อีก 4 ครั้งถ้ามันเจอปีที่ได้เป็นปี 1970 ตั้งแต่เพิ่มเงื่อนไขมา เวลาที่ได้ไม่เคยมีปัญหาอีกเลย

ส่วนการนำค่าที่ได้มาใช้งาน ขออธิบายเพิ่มเติมหน่อยว่า ค่าของวันเวลาที่ได้จะเป็น ออปเจ็คแล้วแต่เราว่าจะสกัดค่าอะไรมาใช้งานบ้าง เช่นถ้าต้องการสกัดเอา hh:mm ก็จะเรียกใช้แบบนี้ newtime->tm_hour จะได้เลข 0–24 และ newtime->tm_min จะได้เลข 0–60

ฟังก์ชันนี้มีการส่งค่ากลับด้วย (return) ดังนั้นถ้าท่านเห็นคำว่า NowString() ที่ไหนมันก็คือ วัน เดือน ปี และเวลา ที่ดึงจาก NTP มาแสดงนั่นเองครับ

// *** (17) CURRENT DATETIME ***String NowString() {int getcount = 1;
time_t now = time(nullptr);
struct tm* newtime = localtime(&now);
String myyear = String(newtime->tm_year + 1900);
//ถ้าปียังเป็นปี 1970 ให้ดึงค่าเวลาใหม่ พยายามสูงสุด 4 ครั้ง
while(myyear == "1970" && getcount <= 4) {
time_t now = time(nullptr);
struct tm* newtime = localtime(&now);
myyear = String(newtime->tm_year + 1900);
vTaskDelay(100 / portTICK_PERIOD_MS); getcount++;
}
String tmpNow = "";
tmpNow += String(newtime->tm_year + 1900);
tmpNow += "-";
tmpNow += String(newtime->tm_mon + 1);
tmpNow += "-";
tmpNow += String(newtime->tm_mday);
tmpNow += " ";
tmpNow += String(newtime->tm_hour);
tmpNow += ":";
tmpNow += String(newtime->tm_min);
tmpNow += ":";
tmpNow += String(newtime->tm_sec);
return tmpNow;}

ขออธิบายเพิ่มเติม เกี่ยวกับตัวแปร newtime ว่ามันเป็นออปเจ็คเก็บข้อมูล เราสามารถสกัดค่าต่างๆ ออกมาใช้งานได้ตามตัวอย่างข้างล่างนี้

struct tm* newtime = localtime(&now);newtime->tm_sec;         /* วินาที,  range 0 to 59      */
newtime->tm_min; /* นาที, range 0 to 59 */
newtime->tm_hour; /* ชั่วโมง, range 0 to 23 */
newtime->tm_mday; /* วันที่, range 1 to 31 */
newtime->tm_mon; /* เดือน, range 0 to 11 */
newtime->tm_year; /* ปีคริสศักราช ตั้งแต่ 1900 */
newtime->tm_wday; /* วัน, range 0 to 6 */
newtime->tm_yday; /* วันใน 1 ปี, range 0 to 365 */
newtime->tm_isdst; /* daylight saving time */

18) FUNCTION GETDHT11

เพื่อความสะดวกในการใช้งาน ผู้เขียนเลยสร้างฟังก์ชัน GetDHT11() ขึ้นมา การทำงานของฟังก์ชันนี้ จะทำการอ่านค่าอุณหภูมิที่มีหน่วยเป็นองศาเซลเซียส และค่าความชื้นสัมพันธ์มีหน่วยเป็นเปอร์เซ็นต์จากเซ็นเซอร์ DHT11

สร้างเงื่อนไขเพิ่มเติมคือ ถ้าไม่สามารถอ่านค่าจากเซ็นเซอร์ได้ให้ทดลองใหม่อีก 4 ครั้ง แต่ถ้าไม่ได้จริงๆ ให้เอาค่าอุณหภูมิและความชื้นสัมพันธ์สำรองของครั้งที่ผ่านมาใช้งาน และส่งข้อความแจ้งเตือนไปยัง LINE NOTIFY

ทำไมต้องอ่านค่าจากเซ็นเซอร์แค่ 4 ครั้ง ?

เท่าที่ผู้เขียนได้ทดสอบอ่านค่าจากเซ็นต์เซอร์ DHT11 / DHT22 ถ้าเราลูปอ่านค่ามันถี่เกินไปก็จะเจอกับปัญหาคือไม่สามารถอ่านค่าได้ มันเป็นเรื่องปกติที่เจอในเซ็นเซอร์ตัวนี้ ให้ลองอ่านค่าใหม่ซ้ำไม่เกิน 4 ครั้งเดี่ยวก็ได้ ผู้เขียนได้ทดลองมาหลายร้อยครั้งแล้วครับ

แต่ถ้าเซ็นต์เซอร์มันเสียจริงๆ ท่านก็จะได้รับการแจ้งเตือนจาก LINE NOTIFY ท่านก็แค่เอาตัวใหม่ไปเปลี่ยนซะก็จบแล้ว เพราะถ้าเซ็นต์เซอร์มันใช้ไม่ได้ก็แน่นอนว่า จะอ่านอีกกี่ร้อยครั้งก็ไม่ได้แน่นอน ผู้เขียนก็ต้องยอมให้ปล่อยผ่านไปไม่เช่นนั้น มันก็จะอ่านวนไปอยู่แบบนี้

// *** (18) FUNCTION GetDHT11 ***void GetDHT11(int *dht) {//กำหนดค่าตัวแปรอุณหภูมิ
byte temperature = 0;
//กำหนดค่าความชื้นสัมสัทธ์
byte humidity = 0;
int count = 1;//อ่านค่าจากเซ็นเซอร์ DHT11
dht11.read(DHTPin, &temperature, &humidity, NULL);
//ถ้าค่าอุณหภูมิ = 0 ให้อ่านค่าจากเซ็นเซอร์ใหม่ 4 ครั้ง
while(temperature == 0 && count <= 4) {
//แสดงข้อความใน Serial Monitor
Serial.print(NowString());
Serial.println(", DHT11 get failed ");
Serial.print(count);
//อ่านค่าจากเซ็นเซอร์ DHT11
dht11.read(DHTPin, &temperature, &humidity, NULL);
vTaskDelay(1500/ portTICK_PERIOD_MS); //ส่งข้อความให้ line notify แจ้งเตือน
if(count == 4) NotifyLine("DHT11 get failed 4 times.");
count++;}//นำข้อมูล temperature & humidity ครั้งที่ผ่านมาแสดง
if
(temperature == 0) temperature = oldtemperature;
if(humidity == 0) humidity = oldhumidity;
//แสดงข้อความใน Serial Monitor
Serial.print(NowString());
Serial.print(", Humidity: ");
Serial.print(humidity);
Serial.print("% | ");
Serial.print("Temperature: ")
Serial.print(temperature);
Serial.println("C");
//เก็บสำรองข้อมูล temperature & humidity
oldtemperature = temperature;
oldhumidity = humidity;
dht[0] = humidity;
dht[1] = temperature;
}

19) FUNCTION WifiPercentSignal

ค่าสัญญาณ WIFI ที่ดีจะดูจากค่า RSSI และ SNR ในฟังก์ชันนี้จะดูที่ค่า RSSI (Recieved Signal Strength Indicator) หน่วยที่ได้จะเป็น dBm มีค่าประมาณตั้งแต่ 0 ถึง -105 แต่เท่านี่ผู้เขียนลงมือทดสอบจริงในภาคสนาม พบว่าค่า RSSI ที่ยอมรับได้ว่า WiFi มีคุณภาพสัญญาณยังใช้ได้อยู่ ก็ประมาณ -75 dBm เพราะถ้ามากกว่านี้ก็จะใช้งานได้ช้าเพราะมันเปลี่ยน Modulation ใหม่ให้ระยะไกลขึ้นเพื่อพยามยามรักษาการเชื่อมต่อ แต่แน่นอนว่าแบนด์วิธที่ได้ก็จะต่ำสุดเช่นกัน จะเร่ิมมีหลุดให้เห็น

ดังน้ันผู้เขียนจึงกำหนดให้ค่า RSSI ต่ำสุดเป็น -90 และสูงสุดเป็น -40 จากนั้นก็แมพค่า RSSI ที่ได้รับเป็นเปอร์เซ็นต์และส่งค่าคืนกลับไป

ทำไมต้องอ่านแม็พค่าเป็นเปอร์เซ็นต์ ?

ค่าที่ได้จากการแม็พจะมีค่าระหว่าง 0–100 เราจะนำค่านี้ส่งให้ Widget แสดงใน Blynk Mobile APP คนทั่วไปได้จะได้เข้าใจได้ง่ายๆ เพราะค่าอะไรที่ออกมาในรูปแบบเปอร์เซ็นต์แล้วจะเข้าใจกันได้ง่าย หรือจะเอาไปพล๊อตกราฟต่อก็ได้นะครับ

// *** (19) FUNCTION WifiPercentSignal ***int WifiPercentSignal() {//หาค่าสัญญาณ RSSI ของ WIFI แล้ว MAP ให้อยู่ในรูปของเปอร์เซ็นต์
CurrentWiFiSignal = WiFi.RSSI();
if(CurrentWiFiSignal > -40) CurrentWiFiSignal = -40;
if(CurrentWiFiSignal < -90) CurrentWiFiSignal = -90;
WifiSignal = map(CurrentWiFiSignal, -40, -90, 0, 100);return WifiSignal;}

20) FUNCTION SoilPercentValue

ESP32 สามารถอ่านค่าแบบอนาล๊อกได้หลายขา GPIO และมีความละเอียด 12 บิต (0–4095) ไม่เหมือน ESP8266/NodeMCU ที่มีขาอนาล๊อกให้อ่านค่าแค่ 1 ขา (A0) และอ่านความละเอียดได้แค่ 10 บิต (0–1023)

ในฟังก์ชันนี้จะทำการแมพค่าที่อ่านได้จากเซ็นเซอร์วัดความชื้นในดิน และคืนกลับไปให้อยู่ในรูปแบบของเปอร์เซ็นต์ และมีการแสดง log ใน Serial Monitor

ค่าต่ำสุดและสูงสุดจากเซ็นเซอร์ได้มาอย่างไร ?

จากโค๊ดข้างล่างค่าต่ำสุดและสูงสุดที่อ่านได้จากเซ็นเซอร์วัดความชื้นในดินจะเท่ากับ 800 และ 4000 ค่านี้มาจากการลงสนามจริงทดสอบ โดยการนำเซ็นเซอร์ไปอ่านค่ากับดินที่แห้งและดินที่เปียกจริงครับ ต้องบอกไว้ก่อนว่าเซ็นเซอร์แต่ละตัวจะให้ค่าไม่เท่ากันดังนั้นเราต้องทดลองทำการ calibrate เองครับ

ยกเว้นเสียแต่ท่านใช้เซ็นเซอร์วัดความชื้นในดินของแบรนด์ มียี่ห้อ ที่ทำการ calibrate ค่ามาแล้ว มีไลบรารี่ให้พร้อมใช้เลย แบบนี้ดีมากครับ แต่ก็ต้องแลกมากับค่าตัวที่สูงหน่อยมีหลายรุ่น ตั้งแต่หลักพันถึงหลักหมื่นบาทเลยนะครับ

// *** (20) FUNCTION SoilPercentValue ***int SoilPercentValue() {//อ่านค่าจากเซ็นเซอร์อนาล๊อก Soil Sensor
CurrentSoilValue = analogRead(SoilSensorPin);
//กำหนดค่าต่ำสุดและสูงสุดของเซ็นเซอร์ที่อ่านได้
if(CurrentSoilValue > 4000) CurrentSoilValue = 4000;
if(CurrentSoilValue < 800) CurrentSoilValue = 800;
//แม๊พค่าให้อยู่ในรูปของเปอร์เซ็นต์
MapReadSoilValue = map(CurrentSoilValue, 800, 4000, 100, 0);
//แสดงข้อความใน Serial Monitor
Serial.print(NowString());
Serial.print(", Soil sensor value: ");
Serial.println(MapReadSoilValue);
return MapReadSoilValue;}

21) FUNCTION LdrPercentValue

ในฟังก์ชันนี้จะทำการแมพค่าที่อ่านได้จากเซ็นเซอร์วัดความเข้มแสงและคืนกลับไปให้อยู่ในรูปแบบของเปอร์เซ็นต์ และมีการแสดง log ใน Serial Monitor

// *** (21) FUNCTION LdrPercentValue ***int LdrPercentValue() {//อ่านค่าจากเซ็นเซอร์อนาล๊อก LDR
CurrentLightValue = analogRead(LdrSensorPin);
if(CurrentLightValue > 4000) CurrentLightValue = 4000;
if(CurrentLightValue < 400) CurrentLightValue = 400;
//แม๊พค่าให้อยู่ในรูปของเปอร์เซ็นต์
MapReadLightValue = map(CurrentLightValue, 400, 4000, 100, 0);
//แสดงข้อความใน Serial Monitor
Serial.print(NowString());
Serial.print(", Light sensor value: ");
Serial.println(MapReadLightValue);
return MapReadLightValue;}

22) TASK1

การสร้างทาสก์ขึ้นมาใช้งาน จำเป็นต้องระบุฟังก์ชันทำงานด้วย หรือเรียกอีกอย่างได้ว่ามันคือโปรแกรมย่อยที่ทำงานในทาสก์นั่นเอง มันไม่มีการส่งค่าคืนและเราไม่สามารถออกจากมันได้

ฟังก์ชันนี้ทำงานแบบนี้ครับ มันจะลูปเฝ้าดูค่าเปอร์เซ็นต์ความชื้นในดินว่าต่ำกว่าเกณฑ์ที่กำหนดหรือไม่ เราสามารถกำหนดค่าได้เองที่ฟังก์ชัน setup() โดยค่าที่กำหนดเร่ิมต้นคือ 40% (PercentWatering) ถ้าพบว่าค่าความชื้นในดินต่ำกว่า 40% ก็หมายความว่าถึงเวลาที่รดนำ้ต้นไม้แล้ว มันจะสั่งให้ขา WateringPin มีสถานะเท่ากับ 0 ทันที

ส่วนรอบความถี่ในการเช็คเปอร์เซ็นต์ความชื้นในดิน (TimeSoilCheck) ค่านี้กำหนดเร่ิมต้นคือ ทุกๆ 1 นาที ถ้าท่านต้องการเปลี่ยนแปลงก็สามารถแก้ไขได้ที่ฟังก์ชัน setup() ผู้เขียนคิดว่าถ้าเอาไปใช้งานจริงทุกๆ 1 ชั่วโมงก็ยังได้เลยนะครับ

ค่าเปอร์เซ็นต์เท่าไหร่จึงจะสั่งรดน้ำ ?

ต้องมีคนสงสัยเป็นแน่ว่าจะตั้งค่าเปอร์เซ็นต์เป็นเท่าไหร่ บอกได้เลยว่าตอบอยากครับคำถามนี้ เพราะผู้เขียนไม่รู้ว่าท่านเอา “ใส่ใจ” ไปใช้กับพืชชนิดไหน ต้นไม้อะไร ผู้เขียนขอแนะนำว่าให้ท่านใช้วิธีการสังเกตดูครับ ก่อนอื่นต้องรู้ว่าความชื้นของดินประมาณไหนที่ท่านต้องการรดน้ำ จากนั้นให้เอาเซ็นต์เซอร์จริงไปวัดดูว่าเปอร์เซ็นต์ความชื้นในดินตอนนั้นเป็นเท่าไหร่ แล้วท่านก็ไปตั้งค่า PercentWatering ใหม่ในฟังก์ชัน setup() อีกทีครับ ทำแบบนี้ก็จะได้ดังใจที่ท่านต้องการแน่นอน

// *** (22) MULTITASK : TASK1 ***/*
* MULTITASK
* TASK1: ลูปเช็คค่าความชื้นดิน ถ้าน้อยกว่าเปอร์เซ็นต์ที่ตั้งไว้ ให้รดน้ำตามเวลาที่กำหนด
*/
void Task1(void *p) { while(1) { //ค่า PercentWatering เป็นค่าความชื้นของดิน
//PercentWatering ต้องไม่เท่ากับ 0
if(SoilPercentValue() < PercentWatering and SoilPercentValue() != 0) {
//เรียกใช้ฟังก์ชันรดน้ำ
digitalWrite(WateringPin, 0);
} //if //เข้าโหมดรอตามเวลาที่กำหนด
//ตัวอย่าง default = 60000 = 1 นาที
vTaskDelay(TimeSoilCheck / portTICK_PERIOD_MS);
} //while
}

23) TASK2

การสั่งให้รดน้ำต้นไม้ของ “ใส่ใจ” จะทำได้ 3 วิธีคือ

  1. การรดน้ำต้นไม้แบบอัตโนมัติถ้าความชื้นในดินต่ำกว่าเกณฑ์ที่กำหนด
  2. ใช้ Blynk Mobile APP สั่งรดน้ำต้นไม้โดยการกดที่ Widget Button
  3. สั่งรดน้ำต้นไม้โดยการกดปุ่มสวิตซ์หน้าตู้คอนโทล

ทำไมต้องการมีการรดน้ำต้นไม้แบบที่ 3 ด้วย ? แนวคิดเป็นแบบนี้ครับ ถ้าวันหนึ่งท่านอยู่ที่หน้าตู้คอนโทลแล้วอยากสั่งรดน้ำต้นไม้ตอนนั้นเลย อาจจะด้วยเหตุผลอะไรก็ตาม ท่านก็แค่กดปุ่มสวิตซ์แค่นั้น ไม่ต้องล้วงเอามือถือขึ้นมาเปิด APP ให้เสียเวลาสะดวกกว่ากันครับ

หน้าที่ของทาสก์นี้ก็คือ ลูปสังเกตสถานะของ SwitchPin ถ้าเมื่อไหร่มันเปลี่ยนสถานะเป็น 0 ก็หมายความว่าถึงเวลาที่รดนำ้ต้นไม้แล้ว มันจะสั่งให้ขา WateringPin มีสถานะเท่ากับ 0 ทันที

// *** (23) MULTITASK : TASK2 ***/*
* MULTITASK
* TASK2: ลูปเช็คสถานะของสวิตซ์ ถ้ามีการกดสวิตซ์จะเป็นการเปิดปั๊มรดน้ำ
*
void Task2(void *p) {//กำหนดค่าตัวแปร state เท่ากับ 0 เป็นค่าเร่มต้น (สถานะปิด)
boolean state = 0;
//กำหนดค่าตัวแปรเป็นตัวเลข 0 กับ 1
boolean oldState;
//กำหนดค่าตัวแปรเป็นตัวเลข 0 กับ 1
boolean data;
while(1) {

//อ่านค่า SwitchPin ในรูปแบบตัวเลข 0 และ 1 (0 คือการกดสวิตซ์)
data = digitalRead(SwitchPin);
//ถ้ามีการกดสวิตซ์ ค่าที่อ่านได้เท่ากับ 0 และมีการกดเพียงครั้งเดียว
//มีค่าที่อ่านได้ก่อนหน้าเท่ากับ 1
if(data == 0 and oldState == 1) {
//รอ 10 มิลลิวินาที เพื่อป้องกันการอ่านค่าผิดพลาด
//ที่เกิดขั้นในระหว่างการเกิดผิวหน้าสัมผัสตอนกดสวิตซ์
vTaskDelay(10 / portTICK_PERIOD_MS);
//อ่านค่าข้อมูลขาดิจิตอล SwitchPin อีกครั้งถ้าพบว่าค่ายังเท่ากับ 0
//แสดงว่ามีการกดสวิตซ์จริง
if(digitalRead(SwitchPin) == 0) {
//เรียกใช้ฟังก์ชันรดน้ำ
digitalWrite(WateringPin, 0);
} //if
} //if
vTaskDelay(100 / portTICK_PERIOD_MS); //กำหนดตัวแปร oldState เท่ากับค่าที่อ่านได้
oldState = data;
} //while}

24) TASK3

ฟังก์ชันนี้หน้าที่เดียวที่มันสนใจคือการลูปเช็คสถานะขา WateringPin ทันทีที่สถานะขาของ WateringPin เท่ากับ 0 (มีการรดน้ำต้นไม้) มันก็จะสั่งให้หลอด LED ที่ต่อกับบอร์ดกระพริบ และส่งข้อมูลอัพเดทแจ้งให้ Blynk Server ทราบ ซึ่งจะทำให้ Widget LED ที่อยู่ใน Blynk Mobile APP กระพริบตามด้วยเหมือนกัน

// *** (24) MULTITASK : TASK3 ***/*
* MULTITASK
* TASK3: ลูปเช็คค่าขา WateringPin ถ้าเปลี่ยนสถานะเป็น 0
* แสดงว่าปั๊มทำงานให้หลอด LED กระพริบ
*/
void Task3(void *p) { while(1) {//อ่านค่าสถานะของ WateringPin, 0 ปั้ม่ทำงาน , 1 ปั๊มไม่ทำงาน
//ถ้าปั้มทำงาน
if(digitalRead(WateringPin) == 0) {
//ให้หลอด LED ติด
digitalWrite(LEDWateringPin, HIGH);
//ให้หลอด LED บน app Blynk ติด
LEDWatering.on();
//รอ .5 วินาที
vTaskDelay(500 / portTICK_PERIOD_MS);
} //if //ให้หลอด LED ดับ
digitalWrite(LEDWateringPin, LOW);
//ให้หลอด LED บน app Blynk ดับ
LEDWatering.off();
//รอ .5 วินาที
vTaskDelay(500 / portTICK_PERIOD_MS);
} //while}

25) TASK4

การรีเซ็ทเร้าเตอร์ มีหลายสาเหตุเช่น แฮงค์ทำให้ใช้ WiFi ไม่ได้แบบนี้เป็นเรื่องธรรมดามากครับ ทุกคนต้องเคยรีเซ็ทเร้าเตอร์เป็นแน่แท้ หรือเกิดจากไฟดับอันนี้ก็สามารถเกิดขึ้นได้เหมือนกัน ปัญหาใหญ่หลังจากที่เร้าเตอร์รีบูตก็คือบอร์ด ESP32 มันไม่เชื่อมต่อให้เองเหมือนโทรศัพท์มือถือของเรา ดังน้ันเราต้องป้องกันปัญหานี้เพราะการนำไปใช้งานจริงต้องเจอปัญหานี้ 100%

ทาสก์นี้จะทำการเช็คสถานะการเชื่อมต่อ WiFi ทุกๆ 5 วินาที และเมื่อมันพบว่าสถานะการเชื่อมต่อไม่เท่ากับ “WL_CONNECTED” แสดงว่า WiFi หลุดการเชื่อมต่อแน่นอน มันก็จะพยายามเชื่อมต่อใหม่ให้เองแบบอัตโนมัติ

// *** (25) MULTITASK : TASK4 ***/*
* MULTITASK
* TASK4: ลูปเช็คสัญญาณ wifi ถ้าติดต่อไม่ได้ให้ต่อใหม่
*/
void Task4(void *p) { while(1) { //เช็คว่าติดต่อ wifi ได้หรือไม่ ถ้าไม่ได้ ให้ reconnect ใหม่
if(WiFi.status() != WL_CONNECTED) {
digitalWrite(LEDWiFiPin, LOW);
Serial.println("WiFi Disconnected.");
WiFi.begin((char*)ssid, (char*)pass); } else { digitalWrite(LEDWiFiPin, HIGH);
}
vTaskDelay(5000 / portTICK_PERIOD_MS); } //while}

26) TASK5

การรดน้ำจะเกิดขึ้นได้ก็ต่อเมื่อขา WateringPin มีสถานะเท่ากับ 0 และมี 3 วิธีดังที่กล่าวไว้ข้างบน จากน้ันจะสั่งให้ขา RelayPin ให้มีสถานะเป็น 0 นั่นก็คือการเปิดปั๊มน้ำให้ทำงานนั่นเอง นอกจากนั้นยังมีการแจ้งอัพเดทไปยัง Blynk Server ให้ Widget LED ติดเพื่อแสดงว่ากำลังมีการรดน้ำ และ Widget Button มีสถานะเป็นทำงานด้วย

ระยะเวลาในการรดน้ำ ท่านสามารถแก้ไขได้ที่ตัวแปร TimeWatering ในฟังก์ชัน setup() ส่วนการแจ้งเตือนและ log จะมีการแสดงข้อความที่ Serial Monitor / Blynk Widget Terminal และส่งข้อความเข้า LINE NOTIFY ก่อนและหลังการรดน้ำต้นไม้

Relay แบบโซลิคสเตตรีเลย์ Solid State Relay คืออะไร ?

Relay โมดูลทั่วไปจะ เปิด ปิดวงจรด้วยแม่เหล็กไฟฟ้า เมื่อมีกระแสไหลผ่านขดลวดเหนี่ยวนำ ก็จะคล้ายกับแม่เหล็กที่สามารถส่งแรงผลักหรือดูดได้ เราจะได้ยินเสียง “แป๊ก” ตอนที่ Relay ทำงาน ถ้าเอามาเปิด ปิดไฟ ทั่วไปจะทำงานได้เป็นอย่างดี เพราะมันรองรับถึง 10A 250VAC แต่ก็มีข้อเสืยคือพอใช้ไปนานๆ อาจเกิดปัญหาฝุ่นเกาะหน้าสัมผัส ทำให้ทำงานผิดพลาดได้ อีกอย่างถ้าใช้กับปั๊มน้ำขนาดใหญ่ ช่วงที่ปั๊มเริ่มทำงานจะเกิดไฟกระชาก บางครั้งทำให้หน้าสัมผัสมีปัญหา ผู้เขียนเจอมาหลายตัวเหมือนกัน

ต้นแบบ “ใส่ใจ” ที่ใช้งานจริงจะใช้ปั๊มน้ำแรงดันแบบ 24VDC — 5A สำหรับรดน้ำต้นไม้ ดังนั้นผู้เขียนจึงใช้ โซลิคสเตตรีเลย์ Solid State Relay (สวิตช์อิเล็กทรอนิกส์) แทนครับ SSR มันรองรับถึง 10A 10–36VDC และใช้เทคโนโลยีของ Semiconductor ทำให้ไม่มีชิ้นส่วนที่เคลื่อนที่ มันไม่มีหน้าสัมผัสไม่มีเสียงตอนทำงาน ไม่เกิดปัญหาเรื่องฝุ่นละออง ทำงานได้รวดเร็ว ใช้พลังงานควบคุมต่ำ สัญญาณรบกวนต่ำ ทนไฟกระชากได้ดี ข้อเสียหนึ่งเดียวคือราคาสูงกว่า Relay โมดูลทั่วไป แต่ก็ไม่แพงมาก ผู้เขียนแนะนำเป็นอย่างยิ่งถ้าใช้งานจริงให้ใช้ โซลิคสเตตรีเลย์ Solid State Relay ในการควบคุมปั๊มน้ำ

// *** (26) MULTITASK : TASK5 ***/*
* MULTITASK
* TASK5: รดน้ำต้นไม้
*/
void Task5(void *p) { while(1) { if(digitalRead(WateringPin) == 0) { //เปิดปั๊มรดน้ำต้นไม้
digitalWrite(RelayPin, LOW);
//อัพเดทสถานะของ LED Relay บน Blynk server
LEDWatering.on();
Blynk.virtualWrite(Widget_WateringButton, 1);
//แสดงข้อความบน blynk terminal
terminal.print(NowString());
terminal.println(", Start");
//แสดงข้อความบนคอนโซล
Serial.print(NowString());
Serial.println(", Plant watering...");
//ส่งข้อความให้ line notify แจ้งเตือน
NotifyLine("Plant watering...");
//เปิดปั๊มรดน้ำตามเวลาที่กำหนด
vTaskDelay(TimeWatering / portTICK_PERIOD_MS);
//ปิดปั๊มรดน้ำต้นไม้
digitalWrite(RelayPin, HIGH);
//เปลี่ยนสถานะ WateringPin เท่ากับ 1 เพื่อหยุดรดน้ำต้นไม้
digitalWrite(WateringPin, 1);
//อัพเดทสถานะของ LED Relay บน Blynk server
LEDWatering.off();
Blynk.virtualWrite(Widget_WateringButton, 0);
//แสดงข้อความบนคอนโซล
Serial.print(NowString());
Serial.println(", Stop watering.");
//ส่งข้อความให้ line notify แจ้งเตือน
NotifyLine("Stop watering.");
terminal.print(NowString());
terminal.println(", Stop");
}//if
} //while
}

จากโค๊ดทั้ง 26 ส่วนที่ได้อธิบายไป มาถึงตอนนี้ผู้เขียนคิดว่าท่านคงเห็นภาพการพัฒนาโปรแกรมด้วย ESP32 แบบมัลติทาสก์กันแล้ว การทำงานแบบอิสระแบ่งออกเป็นทาสก์ๆ ทำให้เราจัดการอะไรได้ง่าย ลดความซับซ้อนการเขียนโปรแกรมลง สำหรับผู้เขียนการใช้ FreeRTOS คู่กับ ESP32 นี่มันเป็นอะไรที่ยอดเยี่ยมมากครับ

ใส่ใจ มีอะไรที่ต้องพัฒนาเพิ่มเติมไหม ?

โค๊ดของใส่ใจที่นำเสนอไป สามารถนำไปปรับใช้งานจริงได้ สำหรับผู้เขียนแล้วมันยังไม่ถึงที่สุด ต้องพัฒนาเพิ่มเติมอีก ที่วางแผนไว้ก็มีดังนี้ครับ

  1. เพิ่มการจัดการ WIFI ในกรณีที่ต้องการเปลี่ยน SSID ใหม่โดยที่ไม่ต้องอัพโค๊ดใหม่ อาจจะเพิ่มสวิตซ์ซักตัว พอกดค้างก็จะทำการเปลี่ยนโหมด ESP32 เป็น Access point และเข้าไปปรับแต่งค่าต่างๆ
  2. ให้ ESP32 เป็น Web Server ด้วย เพื่อรีโมตเข้าไปเปลี่ยนค่าพารามิเตอร์ต่างๆ ในฟังก์ชัน setup() โดยค่าคอนฟิคต่างๆ จะถูกเก็บไว้ใน EEPROM
  3. เพิ่ม RESTful API เพื่อจัดการกับค่าคอนฟิคต่างๆ ใน EEPROM
  4. แยกเซ็นต์เซอร์ออกไปเป็น node ต่างหาก เพื่อความสะดวกในการใช้งานจริง เพราะหน้างานเป็นไปได้สูงว่าเซ็นเซอร์จะอยู่ห่างจากตัวบอร์ด ESP32 มาก
  5. เซ็นต์เซอร์วัดค่าต่างๆ อาจมีมากกว่า 1 ตัว เพื่อหาค่าเฉลี่ยมาใช้งาน
  6. นำเทคโนโลยี LoRaWAN มาช่วยในการรับส่งข้อมูลระยะทางไกลระหว่างตัว Node กับ ESP32
  7. เงื่อนไขที่ ใส่ใจ ใช้ในเวอร์ชันนี้ (version 2.5) จะเป็น Rule Base คือ ใช่ กับ ไม่ใช่ มันสามารถทำงานได้ดีครับ แต่ในความเป็นจริงอาจมีคำว่า อาจจะ อยู่ด้วย ดังนั้นอาจนำ Fuzzy Logic มาช่วยตัดสินใจด้วย
  8. และสุดท้ายที่ผู้เขียนกำลังศึกษาอยู่คือการนำ Simple Machine Learning - Artificial Neural Network System มาใช้ใน ESP32 ซึ่งสามารถใช้ Arduino-IDE เขียนพัฒนาได้ครับ

ทั้งหมดจะเป็นโรดแมพ “ใส่ใจ” เวอร์ชัน 3 และ 4 ผู้เขียนคิดว่าการวางแผนไว้ล่วงหน้าจะทำให้เรารู้ว่าต้องทำอะไรต่อ ซักวันมันจะเป็นอย่างที่ผมตั้งใจไว้ และแน่นอนว่าส่ิงนี้จะเป็นประโยชน์ให้กับอีกหลายคนนำไปประยุกต์ใช้งาน ผมหวังไว้เช่นนั้นครับ

พบกับใหม่ตอนต่อไป “ตอนที่ 10 จากต้นแบบ ใส่ใจ มาเป็นตู้คอนโทลใช้งานจริง”

ผู้เขียนมีความตั้งใจที่จะถ่ายทอดประสบการณ์ การพัฒนาระบบรดน้ำต้นไม้อัจฉริยะ “ใส่ใจ” ด้วย ESP32 เพื่อให้ผู้ที่สนใจ นำไปพัฒนาต่อยอดสร้างนวกรรมใหม่ๆ ขอเป็นส่วนหนึ่งเล็กๆ น้อยๆ ช่วยผลักดันประเทศของเราให้เข้าสู่ยุคดิจิตอล Thailand 4.0

ติดตามข่าวสารเกี่ยวกับ IoT ได้ที่ facebook IoT Thailand

--

--

Responses (5)