ดิว.นินจา

ดิว.นินจา

Monday, April 27, 2020

NETPIE 2020 : อ่านข้อมูลจาก Shadow เพื่อตั้งค่าเริ่มต้นสำหรับ ESP8266/ESP32

สำหรับระบบฝังตัวทั่วไปหรืออุปกรณ์ไอโอที วิธีการหนึ่งที่นิยมใช้ในการเพิ่มความฉลาดกับอุปกรณ์และความเป็นมิตรกับผู้ใช้งาน คือความสามารถในการจดจำสถานะหรือพารามิเตอร์บางตัวที่สำคัญต่อระบบ ตัวอย่างเช่นสีของการแสดงผลที่ผู้ใช้ชอบ พารามิเตอร์ควบคุมที่ผ่านการปรับแต่ง ซึ่งจะถูกเก็บรักษาไว้แม้แต่เวลาที่ไม่มีแหล่งจ่ายไฟเลี้ยง ในไมโครคอนโทรลเลอร์ส่วนใหญ่ในปัจจุบันจะมีหน่วยความจำ 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

แนะนำหนังสือ “ตัวควบคุมป้อนกลับบนอินเทอร์เน็ตโดย ESP8266”

ปัจจุบันเมื่อกล่าวถึงอุปกรณ์ IoT (Internet of Things) คงมีน้อยคนที่จะไม่รู้จัก ในยุคที่การเข้าถึงอินเทอร์เน็ตเป็นกิจวัตรประจำวันของมนุษย์เ...