สำหรับระบบฝังตัวทั่วไปหรืออุปกรณ์ไอโอที วิธีการหนึ่งที่นิยมใช้ในการเพิ่มความฉลาดกับอุปกรณ์และความเป็นมิตรกับผู้ใช้งาน คือความสามารถในการจดจำสถานะหรือพารามิเตอร์บางตัวที่สำคัญต่อระบบ ตัวอย่างเช่นสีของการแสดงผลที่ผู้ใช้ชอบ พารามิเตอร์ควบคุมที่ผ่านการปรับแต่ง ซึ่งจะถูกเก็บรักษาไว้แม้แต่เวลาที่ไม่มีแหล่งจ่ายไฟเลี้ยง ในไมโครคอนโทรลเลอร์ส่วนใหญ่ในปัจจุบันจะมีหน่วยความจำ EEPROM หรือแบ่งพื้นที่บางส่วนของหน่วยความจำแบบ Flash เพื่อเขียนข้อมูลอย่างถาวรได้ หรือระบบที่ใหญ่ขึ้นเช่นราสเบอรี่ พาย อาจเก็บข้อมูลบน SD card ปัญหาคือเราไม่ทราบว่าเมื่อไรแหล่งจ่ายไฟจะเกิดขัดข้องหรือเกิดเหตุการณ์อื่นที่ทำให้ระบบหยุดทำงาน จึงต้องมีการเขียนหน่วยความจำถาวรบ่อยครั้งและมีผลต่ออายุการใช้งาน โดยเฉพาะหน่วยความจำ Flash ที่มีจำนวนครั้งการเขียนน้อยกว่า EEPROM แท้ ทางเลือกหนึ่งที่ทำได้ง่ายบน NETPIE 2020 คือเก็บค่าบน Device Shadow ซึ่งสามารถร้องขอข้อมูลเมื่อไรก็ได้โดยพับลิชไปที่หัวข้อที่กำหนด และรอรับข้อมูลทางช่องทางส่วนตัวของอุปกรณ์ บทความนี้แนะนำวิธีการเขียนโปรแกรมบน ESP8266/ESP32 โดยยกตัวอย่างการตั้งค่าตัวแปรสำหรับนับจำนวนวัตถุอย่างต่อเนื่อง
เพื่อความสะดวกในการทดลอง จะใช้เพียงบอร์ด ESP8266 หรือ ESP32 ที่ผู้อ่านมีอยู่โดยไม่ต้องการเซนเซอร์ภายนอกใดๆ เพื่อความสะดวกจะอ้างถึงบอร์ดโดยรวมว่า NodeMCU ตั้งโจทย์เป็นการนับของในลังที่ลำเลียงมาบนสายพาน ตัวแปรมี 2 ตัวคือ bin แทนจำนวนของลังซึ่งจะเพิ่มค่าทีละหนึ่ง และ totalItems คือจำนวนสิ่งของในลังทั้งหมดที่นับได้สะสม โดยแต่ละลังจะมีจำนวนของเป็นเลขค่าสุ่มระหว่าง 1 ถึง 4 ค่าที่นับได้ทั้งหมดจะเก็บบน Shadow เมื่อหมดเวลาทำงานเราจะปิดอุปกรณ์ไอโอทีนี้ และในเช้าวันใหม่เมื่อเปิดเครื่อง (หรือว่าเกิดไฟฟ้าขัดข้องหรือการเชื่อมต่อกับ NETPIE ณ เวลาใด) อุปกรณ์จะดึงค่า bin และ totalItems ที่นับได้ก่อนหน้านี้มาจาก Shadow และตั้งเป็นค่าเริ่มต้นเพื่อนับต่อได้อย่างถูกต้อง
เริ่มต้นโดยล็อกอินเข้าบัญชี NETPIE 2020 สร้างโปรเจกต์ใหม่ สิ่งที่ต้องการคือ device เพียงตัวเดียว ตั้งชื่อว่า device1 ข้อมูลที่ต้องนำมาใส่ในโปรแกรมด้าน NodeMCU คือ Client-ID, Token, Secret ซึ่งผู้อ่านคงคุ้นเคยกับขั้นตอนดีแล้ว
เนื่องจากเราต้องการตั้งค่าเริ่มต้นด้วยค่าที่มีอยู่ใน Shadow ดังนั้นในการทดลองจะต้องเขียนค่า bin และ totalItems ลงไปบน Shadow ก่อน โดยอาจตั้งค่าเป็น 0 หรือเท่าไรก็ได้แต่ต้องมีชื่อตัวแปรอยู่ มิฉะนั้นเราจะไม่สามารถหาค่าของตัวแปรจากสตริงที่ส่งกลับมาได้ วิธีการที่ง่ายที่สุดคือใช้โปรแกรม MQTTBox ตั้งค่าให้เชื่อมต่อกับ device1 และคลิกปุ่ม [(+)Add publisher] ใส่ข้อความดังแสดงในรูปที่ 1 เพื่อกำหนดค่า bin = 8, totalsItem = 23 โดยสมมุติว่าเป็นค่าที่นับได้ตั้งแต่ครั้งก่อน
รูปที่ 1 การเขียนข้อมูล bin และ totalItems ลงบน Shadow โดย MQTTbox
เมื่อตรวจสอบที่ Shadow ของ device1 จะเห็นค่าถูกเขียนลงบน Shadow ดังรูปที่ 2 ซึ่งต่อไปโปรแกรมบน NodeMCU จะต้องอ่านข้อมูลนี้ในตอนเริ่มต้นทำงานเพื่อตั้งค่าให้กับตัวแปร bin และ totalItems ก่อนที่จะนับค่าต่อไป
รูปที่ 2 ค่าของ bin และ totalItems ที่ถูกเขียนลงบน Shadow
วิธีการอ่านค่าข้อมูลปัจจุบันจาก Shadow ทำได้โดยพับลิชไปที่หัวข้อ
@shadow/data/get โดยสตริงที่พับลิชไปไม่มีความสำคัญ อาจะเป็นสตริงเปล่าก็ได้ ข้อมูลทั้งหมดบน Shadow จะถูกคืนมาที่หัวข้อ
@private/shadow/data/get/response โดยใช้ได้กับ Device Shadow ของตัวเองเท่านั้น
เพื่อความเข้าใจก่อนลงมือเขียนโปรแกรม สามารถใช้ MQTTbox เพื่อตรวจสอบ โดยสร้างหน้าต่าง Publisher และ Subscriber ในหน้าต่างพับลิชด้านซ้ายใส่หัวข้อ
@shadow/data/get ในช่อง Topic to Publish เท่านั้น ในช่อง Payload ปล่อยว่างไว้ ส่วนทางด้าน Subscriber ใส่
@private/# ในช่อง Topic to subscribe และคลิกปุ่ม [Subscribe] การใช้ wild card # มีความหมายว่าสมัครสมาชิกทุกหัวข้อที่ขึ้นต้นว่า
@private/ ซึ่งจะรวม
@private/shadow/data/get/response ที่ใช้รับข้อมูล
คลิกที่ปุ่ม [Publish] บนหน้าต่างด้านซ้าย จะเห็นข้อความถูกส่งกลับมาดังแสดงในรูปที่ 3 ซึ่งในข้อความนี้มีข้อมูลจาก Shadow ที่ต้องการปะปนอยู่กับข้อมูลอื่น สังเกตว่าข้อมูลทั้งหมดมีความยาว 154 ไบต์
รูปที่ 3 ข้อความที่ตอบกลับมาทางหัวข้อ @private/shadow/data/get/response
เมื่อพิจารณาข้อความที่ได้รับจะเห็นว่ามีค่าของ bin และ totalItems ฝังอยู่ในส่วนของ "data"
{"deviceid":"047284e2-ba1a-4969-b9f4-c17b5faa3ad1","data":{"bin":8,"totalItems":23},"rev":505,"modified":1587978653007}
ซึ่งโปรแกรมในส่วนของ NodeMCU ต้องขุดค่า 8 และ 23 ออกมาเพื่อตั้งค่าให้กับตัวแปร bin และ totalItems ก่อนเริ่มทำการนับจำนวนวัตถุต่อ
เมื่อเข้าใจการทำงานแล้ว เราพร้อมที่จะเขียนโปรแกรมสำหรับ NodeMCU ตั้งชื่อว่า
shadowget.ino ส่วนต้นของโปรแกรมเป็นการนิยามไลบรารีและข้อมูลสำหรับ NETPIE 2020
//#include <ESP8266WiFi.h> // uncomment for ESP8266
#include <WiFi.h> // uncomment for ESP32
#include <PubSubClient.h>
// ------------ WiFi and NETPIE 2020 parameters --------------
const char* ssid = "";
const char* password = "";
const char* mqtt_server = "broker.netpie.io";
const int mqtt_port = 1883;
const char* mqtt_Client = "";
const char* mqtt_username = "";
const char* mqtt_password = "";
// ---------------------------------------------------------------
WiFiClient espClient;
PubSubClient client(espClient);
ผู้อ่านต้องเลือกบรรทัดหนึ่งในสองบรรทัดแรกขึ้นกับว่าใช้บอร์ด ESP8266 หรือ ESP32 ต่อมาใส่ค่า SSID, password ของ WiFi router และค่า Client-ID, Token, Secret ของ device1 ที่สร้างตั้งแต่ต้น สองบรรทัดสุดท้ายคือการสร้างออปเจ็ค client สำหรับสื่อสารกับ NETPIE ต่อไปในโปรแกรม
นิยามตัวแปรที่ใช้ในโปรแกรม
long bin; // bin number
long totalItems ; // total number of items
bool initialized = 0; // flag to initialize variables
char msg[100];
String datastr;
สำหรับใช้งานดังนี้
ตัวแปร bin และ totalItems เก็บค่าจำนวนลังและจำนวนสิ่งของทั้งหมดที่นับได้ โดยก่อนนับจำนวนต่อจะต้องตั้งค่าเริ่มต้นจากค่าบน Shadow เสียก่อน
ตัวแปรตรรก initialized ใช้เป็นตัวสื่อสารว่าการตั้งค่าแล้วเสร็จก่อนที่จะเริ่มนับค่า
แอเรย์ของอักขระ msg และตัวแปรสตริง datastr ใช้ในโปรแกรมเพื่อพับลิชค่าให้กับ NETPIE 2020
ในฟังก์ชัน reconnect() ที่ใช้เชื่อมต่อกับ NETPIE เพิ่มคำสั่งสมัครสมาชิกหัวข้อ
@private/#
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection…");
if (client.connect(mqtt_Client, mqtt_username, mqtt_password)) {
Serial.println("connected");
client.subscribe("@private/#");
}
else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println("try again in 5 seconds");
delay(5000);
}
}
}
ส่วนสำคัญในการดึงข้อมูลที่ต้องการออกจากสตริงรวมอยู่ในฟังก์ชัน callback() ที่จะอธิบายในภายหลัง สำหรับการตั้งค่าในฟังก์ชัน setup() ไม่มีคำสั่งสำคัญที่ต้องอธิบาย เพราะเป็นการตั้งค่า WiFi และ NETPIE 2020 ตามปกติ
ในส่วนของฟังก์ชัน loop() เขียนได้ดังนี้
void loop() {
if (client.connected()) {
client.loop();
if (initialized) {
bin++; // bin is incremented by 1
totalItems+=random(1,5); // random number between 1 - 4
datastr = "{\"data\": {\"bin\":" + String(bin) + ", \"totalItems\":" + String(totalItems)+"}}";
Serial.println(datastr);
datastr.toCharArray(msg, (datastr.length() + 1));
client.publish("@shadow/data/update", msg);
}
else {
client.publish("@shadow/data/get",""); // request shadow data
}
}
else {
reconnect();
initialized = 0; // must reinitialize variables
}
delay(5000);
}
โดยเริ่มต้นตรวจสอบว่ายังเชื่อมต่อกับ NETPIE หรือไม่ หากเป็นจริงเรียก client.loop() เพื่อรักษาการเชื่อมต่อไว้ หลังจากนั้นเข้าสู่การนับค่าใหม่ซึ่งจะเริ่มนับต่อเมื่อพบว่าการตั้งค่าเริ่มต้นสำเร็จแล้ว คือ initialized = 1 เท่านั้น หากไม่เป็นจริงก็จะข้ามคำสั่งในเงื่อนไขนี้ทั้งหมดไป เมื่อเพิ่มค่าตัวแปรแล้วจึงพับลิชไปที่หัวข้อ
@shadow/data/update เพื่อเขียนค่าใหม่ลงบน Shadow ของ device1
การนับค่าในส่วนนี้เขียนแสดงเป็นตัวอย่างง่ายๆ เท่านั้น คือจำนวนลังเพิ่มขึ้นหนึ่งหน่วย และสิ่งของในลังเป็นค่าสุ่มระหว่าง 1 – 4 ในทุก 5 วินาที หากนำไปใช้งานจริงผู้อ่านต้องแก้ไขในส่วนนี้ตามความเหมาะสม โดยใช้ค่าจริงจากเซนเซอร์ และต้องตรวจสอบว่าการนับสิ่งของแล้วเสร็จในแต่ละลังก่อนเพิ่มค่าตัวแปรและพับลิชเพื่อเขียนค่าบน Shadow
สำหรับกรณีที่เริ่มเปิดเครื่อง การเชื่อมต่อหลุด หรือรีบูตใหม่หลังไฟดับ ค่าตัวแปร initialized = 0 ดังนั้นจะไม่มีการนับค่าแต่ข้ามไปทำในส่วน
else {
client.publish("@shadow/data/get",""); // request shadow data
}
คือการพับลิชสตริงเปล่าไปที่หัวข้อ
@shadow/data/get ซึ่งก็คือการร้องขอข้อมูลจาก Shadow ตามที่ได้อธิบายข้างต้น ซึ่งจะได้ข้อความกลับเข้ามาในฟังก์ชัน callback() เพราะได้สมัครสมาชิกไว้ในหัวข้อ
@private/#
ในส่วนสุดท้ายของ loop() คือเมื่อพบว่าการเชื่อมต่อกับ NETPIE 2020 ยังไม่สำเร็จหรือหลุด ก็จะทำในส่วนเงื่อนไข
else {
reconnect();
initialized = 0; // must reinitialize variables
}
ก็คือเรียกฟังก์ชัน reconnect() และตั้งค่าตัวแปร initialized เท่ากับ 0 สังเกตว่าในการวนรอบฟังก์ชัน loop() ครั้งแรกจะเข้าสู่เงื่อนไขนี้
บรรทัดสุดท้ายคือการหน่วงเวลา 5 วินาที โดยสมมุติเป็นเวลาในการนับสิ่งของแต่ละลัง
ย้อนกลับมาถึงฟังก์ชันที่เป็นส่วนสำคัญในการรับข้อมูลจาก Shadow คือ callback() และเป็นส่วนที่ซับซ้อนที่สุดของตัวอย่างนี้ เพราะต้องเขียนโค้ดเพื่อดึงค่าตัวแปร bin และ totalItems จากสตริงรวมที่ได้รับมา ภาษาระดับสูงเช่นไพธอนจะมีคำสั่งที่ช่วยในการดำเนินการนี้ได้โดยไม่ยาก แตกต่างจาก C ที่เป็นภาษาระดับกลางที่ต้องใช้หลายขั้นตอน ผู้อ่านที่มีความชำนาญอาจคิดวิธีการได้สั้นกว่านี้ อย่างไรก็ตามโค้ดนี้ทดสอบแล้วใช้ได้ผลดี
ฟังก์ชัน callback() ทั้งหมดในตัวอย่างเขียนได้ดังนี้
void callback(char* topic, byte* payload, unsigned int msglength) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
String message;
for (int i = 0; i < msglength; i++) {
message = message + (char)payload[i];
}
Serial.println(message);
if(String(topic) == "@private/shadow/data/get/response") {
// extract bin and totalItems value from message
String bin_str, totalItem_str;
int data_idx;
data_idx = message.indexOf("bin");
totalItem_str = message.substring(data_idx+5);
data_idx = totalItem_str.indexOf("}");
totalItem_str = totalItem_str.substring(0,data_idx);
data_idx = totalItem_str.indexOf(",");
bin_str = totalItem_str.substring(0,data_idx);
Serial.println("Extract bin and totalItems from Shadow data");
Serial.print("bin = ");
Serial.println(bin_str);
data_idx = totalItem_str.indexOf("totalItems");
totalItem_str = totalItem_str.substring(data_idx+12);
Serial.print("totalItems = ");
Serial.println(totalItem_str);
bin = bin_str.toInt();
totalItems = totalItem_str.toInt();
Serial.println("Variables initialized");
initialized = 1;
}
}
ในช่วงต้นของฟังก์ชันคือการถ่ายข้อความที่รับได้ในแอเรย์ payload สู่ตัวแปร message ที่เป็นแบบสตริง เพราะสามารถดำเนินการต่อได้สะดวกกว่าตามวิธีการที่รองรับ หลังจากนันเข้าสู่เงื่อนไขการตรวจสอบหัวข้อว่าเท่ากับ หรือไม่
"@private/shadow/data/get/response" ถ้าเป็นจริงแสดงว่าคือข้อความจาก Shadow ที่ร้องขอไป ก็จะนำข้อความในตัวแปร message มาเพื่อดึงข้อมูลค่าของ bin และ totalItems จากสตริงทั้งหมด โดยค้นหาตัวชี้ที่ชื่อของตัวแปร และตัดเป็นสตริงย่อยที่อักขระตัวที่คั่นอยู่ แปลงค่าที่ได้ในรูปสตริงให้เป็นค่าตัวเลข และแทนค่าเลขนั้นลงในตัวแปร bin และ totalItems สุดท้ายคือแสดงข้อความ "Variables initialized" ออกพอร์ตอนุกรม และตั้งค่าตัวแปร initialized = 1
เมื่อเขียนโค้ดทั้งหมดลงบนโปรแกรม
shadowget.ino แล้ว คอมไพล์และโหลดลงบน NodeMCU พบว่าสามารถเชื่อมต่อ NETPIE 2020 ได้ แต่หลังจากนั้นไม่มีข้อความใดๆ ออกที่ Serial Monitor เลย ทั้งที่โปรแกรมน่าจะทำงานได้ตามวัตถุประสงค์
ปัญหาที่เกิดขึ้นคือเมื่อย้อนไปดูข้อความจาก Shadow แสดงในรูปที่ 3 พบว่ามีความยาวถึง 154 ไบต์ ซึ่งมากกว่าค่าสูงสุดที่ไลบรารี PubSubClient กำหนด ตรวจสอบได้โดยเข้าไปที่ไฟล์
PubSubClient.h ในไลบรารี (สำหรับระบบปฏิบัติการวินโดวส์อยู่ที่ไดเรคทอรี
\Documents\Arduino\libraries\Pubsubclient\src\ ) เปิดไฟล์ด้วย text editor ที่มีเช่น Nodepad++ เลื่อนมาที่นิยาม
MQTT_MAX_PACKET_SIZE ดังในรูปที่ 4 จะเห็นว่ากำหนดไว้ 128 ซึ่งน้อยกว่าความยาวข้อความที่ส่งมา ทำให้ไม่สามารถรับข้อความได้
รูปที่ 4 ค่าขนาดข้อความสูงสุดที่นิยามในไฟล์ PubSubClient.h
แก้ไขไลบรารีโดยเพิ่มค่า าม
MQTT_MAX_PACKET_SIZE เป็น 256 หรือ 512 คอมไพล์และโหลดโปรแกรม
shadowget.ino ลงบน NodeMCU จะเห็นว่าในครั้งนี้สามารถรับค่าข้อความที่ส่งมาจาก Shadow และตั้งค่าตัวแปร bin = 8 และ totalItems = 23 สำเร็จ และนับต่อจากค่าเริ่มต้นนี้
รูปที่ 5 การตั้งค่าตัวแปรจากค่าบน Shadow สำเร็จ
ถึงแม้ว่าโปรแกรมจะทำงานได้ตามวัตถุประสงค์ แต่ยังมีจุดบกพร่องอยู่เล็กน้อย คือช่วงเวลาตั้งแต่การร้องขอจนถึงการได้รับข้อความจาก Shadow ไม่สามารถกำหนดได้ ผลคือในบางครั้งเงื่อนไขบน loop() พบว่าการตั้งค่ายังไม่สมบูรณ์และส่งการร้องขอไปเป็นครั้งที่สอง ทำให้มีข้อความมาเป็นครั้งที่สองและเกิดการตั้งค่าอีกครั้งทั้งที่โปรแกรมเริ่มการนับแล้ว ทำให้เกิดความผิดพลาดได้ วิธีการแก้คือเพิ่มตัวแปรสถานะอีกตัวหนึ่ง
bool requestsent = 0; // flag to request shadow data only once
เพื่อเป็นตัวกำหนดว่าการร้องขอจะส่งไปเพียงครั้งเดียวเท่านั้น แก้ไขในส่วนเงื่อนไข else ของ if (initialized) ดังนี้
else {
if (!requestsent) { // publish only once
client.publish("@shadow/data/get",""); // request shadow data
requestsent = 1;
}
}
ปัญหาการตั้งค่าซ้อนกันสองครั้งหมดไปเมื่อเพิ่มกลไกนี้เข้าไป
ทดลองกดรีเซ็ตบนบอร์ด NodeMCU เพื่อเริ่มการทำงานใหม่ โปรแกมจะตั้งค่าตัวแปรด้วยค่าปัจจุบันบน Shadow อย่างถูกต้องทุกครั้ง สำหรับการทดสอบการตั้งค่าใหม่เมื่อการเชื่อมต่อ NETPIE ขัดข้อง ท่านไม่จำเป็นต้องไปปิด/เปิด WiFi router หรือพยายามเอาหีบเหล็กไปครอบ NodeMCU วิธีการง่ายๆ คือเปิด MQTTBox และคลิกปุ่มการเชื่อมต่อ ทำให้มีสองอุปกรณ์ที่มีข้อมูลสิทธิ์เดียวกันพยายามเชื่อมต่อ NETPIE 2020 พร้อมกัน ทำให้การเชื่อมต่อของทั้งสองไม่เสถียร สร้างสถานการณ์นี้สัก 10 วินาทีหลังจากนั้นยกเลิกการเชื่อมต่อจาก MQTTBox ทำให้ NodeMCU สามารถเชื่อมต่อ NETPIE ได้สำเร็จอีกครั้งหนึ่ง จะเห็นว่าโปรแกรมจะสามารถตั้งค่าตัวแปร bin และ totalItems ได้หลังการเชื่อมต่อใหม่ ดังแสดงในรูปที่ 6
รูปที่ 6 การตั้งค่าตัวแปรหลังจากสามารถเชื่อมต่อ NETPIE ได้ใหม่
โปรแกรมที่ใช้ในบทความ :
shadowget.ino
No comments:
Post a Comment