ดิว.นินจา

ดิว.นินจา

Wednesday, March 4, 2020

NETPIE 2020 : สร้างตัวรับและแปลคำสั่ง

เนื่องจาก NETPIE2020 เป็นแพลตฟอร์มใหม่ที่เพิ่งเปิดตัว ดังนั้นตัวอย่างที่แสดงบนเว็บจะเน้นการใช้งานขั้นพื้นฐาน โดยเฉพาะด้านการส่งคำสั่งให้กับอุปกรณ์จะสาธิตเพียงการปิด/เปิด LED บนบอร์ด NodeMCU จาก Freeboard ในการสร้างอุปกรณ์ไอโอทีสำหรับงานบางประเภท ผู้ใช้ต้องการที่จะควบคุมหรือสั่งงานโดยชุดคำสั่งจำนวนหนึ่ง เช่นงานควบคุมอุตสาหกรรมจะมีการปรับพารามิเตอร์ PID, setpoints, สถานะการควบคุม ฯลฯ ซึ่งโครงสร้างโปรแกรมตัวอย่างอาจไม่เอื้ออำนวยในการดัดแปลงให้รับคำสั่งจำนวนหลายคำสั่ง เพราะหากเพิ่มโค้ดทั้งหมดลงในฟังก์ชัน callback() จะทำให้มีความยาวและยากต่อการบำรุงรักษาโปรแกรมในภายหลัง ดังนั้นในบทความนี้จึงนำเสนอวิธีการรับและแปลคำสั่งใน NETPIE 2020 โดยจะเปรียบเทียบระหว่างการเขียนโค้ดลงใน callback() กับการสร้างฟังก์ชันแปลคำสั่งในรุปแบบ cmd=value ที่สื่อความหมายกับผู้ใช้ได้ดีกว่า ไม่ต้องแก้ไขฟังก์ชันเดิมหและมีข้อดีอีกประการคือสามารถสั่งงานผ่านพอร์ตอนุกรมหรือ Bluetooth ได้โดยคำสั่งชุดเดียวกัน

การทดลองในบทความนี้จะใช้บอร์ด IGR ที่ผู้เขียนออกแบบให้ใช้งานร่วมกับ NodeMCU ดังในรูปที่ 1 ผู้อ่านสามารถใช้บอร์ดทดลองอื่นได้โดยต่อ LED 3 ดวงเข้ากับ D5, D6, D7 ซึ่งจะตั้งชื่อว่า LAMP, VALVE, FERTILIZER แทนการให้แสง เปิดวาล์วน้ำ และการให้ปุ๋ยสำหรับระบบเรือนต้นไม้ในร่ม

รูปที่ 1 บอร์ด IGR สำหรับ NodeMCU

เนื้อหาในบทความนี้จะเน้นเรื่องการส่งข้อความให้กับบอร์ด ดังนั้นจะไม่อธิบายในส่วนรับข้อมูลจากเซนเซอร์เก็บใน Shadow และแสดงผลที่จะเหมือนกับตัวอย่างบนเว็บของ NETPIE

โปรแกรม NETPIE2020cmd1.ino จะสาธิตการสั่งงานที่แยกโดยหัวข้อ topic ที่ส่วนบนของโปรแกรม นิยามขาที่ใช้งานคือ


#define LAMP D5
#define VALVE D6
#define FERTILIZER D7

และตั้งค่าเป็นเอาต์พุตในฟังก์ชัน setup()


pinMode(LAMP, OUTPUT);    // Grow lamp intensity 0 - 100%
pinMode(VALVE, OUTPUT);    // water valve open 0 - 100%
pinMode(FERTILIZER, OUTPUT);    // turn off/on fertilizer unit

โครงสร้างการรับข้อความของ NETPIE 2020 ที่ใช้ในตัวอย่างคือ อุปกรณ์ NodeMCU ต้องสมัครสมาชิก (subscribe) หัวข้อ @msg ก่อน จึงจะสามารถข้อความที่ส่งมาให้ผ่านฟังก์ชัน callback() ดังนั้นในฟังก์ชัน reconnect() เพิ่มคำสั่ง client.subscribe() ดังนี้


void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection…");
    if (client.connect(mqtt_Client, mqtt_username, mqtt_password)) {
      Serial.println("connected");
      // subscribe to command topics
      client.subscribe("@msg/#");      
    }
    else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println("try again in 5 seconds");
      delay(5000);
    }
  }
}

โดยหัวข้อใช้ wildcard # เพื่อรับข้อความจากหัวข้อใดๆ ที่ขึ้นต้นด้วย @msg/

วิธีการที่ใช้ในตัวอย่างการสั่งงานอุปกรณ์บนเว็บเน็ตพายคือ ใช้ส่วนต่อท้ายหัวข้อ @msg/ เป็นตัวแยก เริ่มต้นจะลองใช้วิธีการนี้เพื่อสร้าง 3 คำสั่ง คือ

  • @msg/fertilizer : สั่งปิด/เปิดการให้ปุ๋ย ข้อความในหัวข้อนี้คือ off หรือ on
  • @msg/lamp : สั่งความสว่างหลอดไฟ ข้อความในหัวข้อนี้เป็นตัวเลข 0 – 100 (เปอร์เซนต์)
  • @msg/valve : สั่งการเปิดวาล์วน้ำ ข้อความในหัวข้อนี้เป็นตัวเลข 0 – 100 (เปอร์เซนต์)

เพิ่มตัวแปลส่วนกลางที่ใช้งาน


int lamp=0, valve=0;  // lamp and valve value between 0 - 100 %
String datastr;       // data string used in command section

และเขียนส่วนคำสั่งทั้งสามลงในฟังก์ชัน callback() ได้ดังนี้


void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String message;
  for (int i = 0; i < length; i++) {
    message = message + (char)payload[i];
  }
  Serial.println(message);
// ----------------- Fertilizer control ----------------------------
  if(String(topic) == "@msg/fertilizer") {
    if (message == "on"){
      digitalWrite(FERTILIZER,1);
      client.publish("@shadow/data/update", "{\"data\" : {\"fertilizer\" : \"on\"}}");
      Serial.println("FERTILIZER ON");
    }
    else if (message == "off") {
      digitalWrite(FERTILIZER,0);
      client.publish("@shadow/data/update", "{\"data\" : {\"fertilizer\" : \"off\"}}");
      Serial.println("FERTILIZER OFF");
    }
  }
// ---------------- Lamp control -------------------
  else if(String(topic) == "@msg/lamp") {
    lamp = message.toInt();
    int lampvalue = map(lamp,0,100,0,1023);
    analogWrite(LAMP, lampvalue);
    datastr = "{\"data\": {\"lamp\":" + String(lamp)+"}}";
    datastr.toCharArray(msg, (datastr.length() + 1));
    client.publish("@shadow/data/update", msg);
    Serial.print("Lamp set to ");
    Serial.print(lamp);
    Serial.println(" %");
    Serial.println(datastr);
  }
// ---------------------- Valve control -----------------------------
  else if(String(topic) == "@msg/valve") {
    valve = message.toInt();
    int valvevalue = map(valve,0,100,0,1023);
    analogWrite(VALVE, valvevalue);
    datastr = "{\"data\": {\"valve\":" + String(valve)+"}}";
    datastr.toCharArray(msg, (datastr.length() + 1));
    client.publish("@shadow/data/update", msg);    
    Serial.print("Valve set to ");
    Serial.print(valve);
    Serial.println(" %");
    Serial.println(datastr);
  }
}

ค่าของ lamp และ valve ที่รับมาในช่วง 0 – 100 จะถูกปรับมาตราส่วนเป็น 0 – 1023 สอดคล้องกับ 10-bit PWM ก่อนส่งออกโดยฟังก์ชัน analogWrite()

ทางด้าน NETPIE 2020 เพิ่มโค้ดใน Device Schema เพื่อจัดเก็บค่าของ 3 พารามิเตอร์นี้ (ใส่ไว้ในไฟล์ schema.JSON ที่รวมอยู่ใน .zip ด้านล่างบทความ)

เปิดหน้า Freeboard สร้าง datasource โดยใช้ข้อมูลจากฟิลด์ Client ID และ Token กรอกในช่อง DEVICE ID และ DEVICE TOKE ตามลำดับ ตัวอย่างนี้ตั้งชื่อว่า igrdatasource หลังจากนั้นสร้าง widgets สำหรับควบคุม 3 ตัวดังในรูปที่ 2 โดย Lamp และ Valve ใช้ slider ปรับค่า 0 – 100 % ส่วน Fertilizer ใช้ toggle เพื่อเปลี่ยนสถานะปิด/เปิด

รูปที่ 2 Freeboard widgets สำหรับควบคุม Lamp, Valve, Fertilizer

รูปที่ 3 แสดงการตั้งค่าสำหรับ Lamp slider ในส่วนการส่งคำสั่งจะใส่ในฟิลด์ ONSTOP ACTION ซึ่งทำงานเมื่อหยุดการเลื่อน slider คำสั่งที่ใช้คือ


netpie["igrdatasource"].publish("@msg/lamp",String(value))

ซึ่งใช้วิธีการพับลิชในหัวข้อ @msg/lamp ส่งค่า value คือค่าตัวเลขที่ตรงกับตำแหน่งของ slider โดยจะต้อง cast เป็นแบบ String ก่อนส่ง

ส่วนในฟิลด์ AUTO UPDATED VALUE ใช้คำสั่ง


datasources["igrdatasource"]["shadow"]["lamp"]

เพื่อนำเอาค่าของ lamp ที่เก็บใน Data Shadow มาอัพเดทให้กับ slider

สำหรับคำสั่งสำหรับ valve slider ก็จะมีลักษณะคล้ายกัน เพียงแต่เปลี่ยนชื่อหัวข้อและตัวแปรที่เก็บใน Data Shadow


ONSTOP ACTION :  netpie["igrdatasource"].publish("@msg/valve",String(value))
AUTO UPDATED VALUE :  datasources["igrdatasource"]["shadow"]["valve"]
รูปที่ 3 หน้าต่างการตั้งค่าสำหรับ Lamp slider

การตั้งค่าสำหรับ valve slider ก็จะมีลักษณะคล้ายกัน เพียงแต่เปลี่ยนชื่อหัวข้อและตัวแปรที่เก็บใน Data Shadow เท่านั้น


ONSTOP ACTION : netpie["igrdatasource"].publish("@msg/valve",String(value))
AUTO UPDATED VALUE : datasources["igrdatasource"]["shadow"]["valve"]

รูปที่ 4 แสดงหน้าต่างการตั้งค่า toggle ปิด/เปิดการให้ปุ๋ย หัวข้อในการส่งคำสั่งคือ @msg/fertilizer และข้อความที่ส่งคือ off และ on เมื่อปุ่มกดอยู่ในสถานะปิดและเปิดตามลำดับ

รูปที่ 4 หน้าต่างตั้งค่า toggle สำหรับปิด/เปิดการให้ปุ๋ย

คอมไพล์และโหลดโปรแกรม NETPIE2020cmd1.ino ลงบน NodeMCU (อย่าลืมแก้ไขข้อมูล WiFi และ NETPIE ให้ตรงกับที่ท่านใช้) เมื่ออุปกรณ์เชื่อมต่อ NETPIE 2020 สำเร็จ ทดลองว่าสามารถสั่งงานจากหน้า Freeboard ได้จริง เปิด Serial Monitor ลองเลื่อน Lamp slider จะเห็นข้อความแสดงดังในรูปที่ 5 แสดงว่า NodeMCU ได้รับข้อความที่ส่งมาจาก slider และ LED สีเหลืองที่จำลองเป็นหลอดไฟในเรือนต้นไม้จะเปล่งแสงด้วยความสว่างที่ปรับโดย slider (รูปที่ 6)

รูปที่ 5 ข้อความที่แสดงเมื่อมีการปรับ Lamp slider
รูปที่ 6 LED ที่แสดงความสว่างของหลอดไฟถูกปรับค่าโดย slider

LED แบบ SMD สีเขียวบนบอร์ด IGR ที่จำลองการทำงานของวาล์วก็จะถูกปรับความสว่างโดย Valve slider ในลักษณะเดียวกัน ส่วนการสั่งงานการให้ปุ๋ยโดยปุ่ม Fertilizer toggle เมื่อกดจะเห็นข้อความแสดงบน Serial Monitor ดังในรูปที่ 7 และ LED สีแดงบนบอร์ดที่จำลองการเปิดให้ปุ๋ยจะติดดังในรูปที่ 8 การสั่งงานเป็นลักษณะเปลี่ยนสถานะ OFF/ON

รูปที่ 7 ข้อความเมื่อกดปุ่ม Fertilizer toggle
รูปที่ 8 LED สีแดงจำลองการเปิดการให้ปุ๋ย

สรุปการส่งคำสั่งโดยวิธีการตามตัวอย่างนี้จะแยกคำสั่งโดยหัวข้อ หากมีคำสั่งเพิ่มเติมก็จะสร้างเงื่อนไข @msg/xxxx เพิ่มเข้าไปในฟังก์ชัน callback() ซึ่งค่อนข้างสะดวกสำหรับการสั่งงานโดย Freeboard widgets แต่ในกรณีที่ต้องการอิมพลิเมนต์เป็นสตริงคำสั่งที่ผู้ใช้สามารถเข้าใจและจดจำได้ง่าย และสามารถส่งคำสั่งผ่านช่องทางอื่น เช่น กล่องข้อความ พอร์ตอนุกรม อาจจะไม่เหมาะสมนัก ยกตัวอย่างการปรับค่าอัตราขยาย PID โดยคำสั่ง @msg/kp/4.7 อาจจะเป็นโครงสร้างคำสั่งที่ผู้ปฏิบัติงานไม่คุ้นเคยถ้าเปรียบเทียบกับ kp = 4.7

ผู้เขียนเองได้ใช้โครงสร้างคำสั่งในรูป command = parameter ใน NETPIE เวอร์ชัน 2015 และสร้างฟังก์ชัน cmdInt() สำหรับแปลคำสั่งในลักษณะนี้ โดยใช้อย่างแพร่หลายทั้งในหนังสือและในงานจริง การปรับโปรแกรมให้รับคำสั่งในแบบแยกโดยหัวข้อในตัวอย่างที่ผ่านมาจะทำให้ต้องแก้ไขโค้ดจำนวนมาก ดังนั้นจึงนำเสนอการสั่งงานโดยใช้หัวข้อคงที่ เช่น @msg/cmd และใช้โครงสร้าง command = parameter ในส่วนของข้อความแทน โดยวิธีการนี้ฟังก์ชัน callback() เมื่อพบว่ามีการส่งหัวข้อ @msg/cmd จะเรียกฟังก์ชัน cmdInt() ทำหน้าที่แปลคำสั่งและทำงานตามต้องการ ทำให้โค้ดใน callback() กระชับ และ cmdInt() ไม่ต้องมีการเปลี่ยนแปลงจากของเดิมนอกจากเพิ่มส่วนเขียนข้อมูลไปยัง Data Shadow หากต้องการ

ในโปรแกรม NETPIE2020cmd2.ino โครงสร้างส่วนใหญ่จะคงเดิม จุดที่แตกต่างคือแก้ไขฟังก์ชัน callback() และเพิ่มเติมฟังก์ชัน cmdInt() และตัวแปรที่ใช้ในฟังก์ชัน โค้ดในส่วน callback() เป็นดังนี้


void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String message;
  for (int i = 0; i < length; i++) {
    message = message + (char)payload[i];
  }
  Serial.println(message);
  if(String(topic) == "@msg/cmd") {
    rcvdstring=message; 
    cmdInt();  // call command interpreter
  }
}

อธิบายการทำงานคือ ถ้าหัวข้อที่รับมาตรงกับ @msg/cmd ก็จะคัดลอกข้อความใส่ในตัวแปร rcvdstring และเรียกฟังก์ชัน cmdInt() ที่จะแปลคำสั่งในตัวแปรนี้

ส่วนโค้ดในฟังก์ชัน cmdInt() เหมือนกับที่เคยใช้กับ NETPIE 2015 เพิ่มเฉพาะการเก็บค่าลงบน Device Shadow ซึ่งจะใช้ในการอัพเดทค่าของ widgets บน Freeboard


void cmdInt(void)
{
    rcvdstring.trim();  // remove leading&trailing whitespace, if any
    // find index of separator ('=')
    sepIndex = rcvdstring.indexOf('=');
    if (sepIndex==-1) {  // no parameter in command
      cmdstring = rcvdstring;
      noparm = 1;   
    }
    else  {
    // extract command and parameter
      cmdstring = rcvdstring.substring(0, sepIndex);
      cmdstring.trim();
      parmstring = rcvdstring.substring(sepIndex+1); 
      parmstring.trim();
      noparm = 0;
    }
    // check if received command string is a valid command
    if (cmdstring.equalsIgnoreCase("lamp"))   {
      if (noparm==1)   {
        Serial.println("Lamp intensity = "+(String)map(lampvalue,0,1023,0,100) + " %");
      }
      else   {  
         parmvalint = parmstring.toInt(); 
         if (parmvalint > 100) parmvalint = 100; // limit to 100%
         else if (parmvalint<0) parmvalint = 0;
         lamp = parmvalint;
         lampvalue = map(lamp,0,100,0,1023);  
         analogWrite(LAMP,lampvalue);
         datastr = "{\"data\": {\"lamp\":" + String(lamp)+"}}";
         datastr.toCharArray(msg, (datastr.length() + 1));
         client.publish("@shadow/data/update", msg);
         Serial.print("Lamp set to ");
         Serial.print(lamp);
         Serial.println(" %");
         Serial.println(datastr);         
      }
    }
    else if (cmdstring.equalsIgnoreCase("valve"))   {
      if (noparm==1)   {
        Serial.println("Valve opening = "+(String)map(valvevalue,0,1023,0,100) + " %");
      }
      else   {  
         parmvalint = parmstring.toInt(); 
         if (parmvalint > 100) parmvalint = 100; // limit to 100%
         else if (parmvalint<0) parmvalint = 0;
         valve = parmvalint;
         valvevalue = map(valve,0,100,0,1023);  
         analogWrite(VALVE,valvevalue);
         datastr = "{\"data\": {\"valve\":" + String(valve)+"}}";
         datastr.toCharArray(msg, (datastr.length() + 1));
         client.publish("@shadow/data/update", msg);    
         Serial.print("Valve set to ");
         Serial.print(valve);
         Serial.println(" %");
         Serial.println(datastr);         
      }
    }
     else if (cmdstring.equalsIgnoreCase("fertilizer"))   { // fertilizer feed
      if (noparm==1)   {
        Serial.print("Current fertilizer state = ");
        Serial.println(fertilizer_state);       
      }
      else   {  
        if (parmstring.equalsIgnoreCase("on")){
          digitalWrite(FERTILIZER,1);
          client.publish("@shadow/data/update", "{\"data\" : {\"fertilizer\" : \"on\"}}");
          Serial.println("FERTILIZER ON");
          fertilizer_state = parmstring;
        }
        else if (parmstring.equalsIgnoreCase("off")) {
          digitalWrite(FERTILIZER,0);
          client.publish("@shadow/data/update", "{\"data\" : {\"fertilizer\" : \"off\"}}");
          Serial.println("FERTILIZER OFF");
          fertilizer_state = parmstring;
        }
        else Serial.println("Invalid fertilizer state");
      }
    }   
    else   { // no match
         Serial.println("Invalid command.");
     }
}

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


  while (Serial.available() > 0)   {  // detect new input
     rcvdstring = Serial.readString();
     newcmd = 1;
  }
  if (newcmd)   { // execute this part only when new command received
    cmdInt();   // invoke the interpreter
    newcmd = 0;
  } 

คอมไพล์และโหลดโปรแกรม NETPIE2020cmd2.ino ลงบน NodeMCU แต่ก่อนจะทดสอบการรับคำสั่ง เราต้องแก้ไขทางด้าน Freeboard ให้ slider และ toggle widgets ส่งคำสั่งในรูปแบบใหม่ คือ มีหัวข้อเป็น @msg/cmd และข้อความ command=parameter โดยสำหรับ Lamp slider แก้ไขดังในรูปที่ 9 คือในฟิลด์ ONSTOP ACTION ใช้คำสั่ง


netpie["igrdatasource"].publish("@msg/cmd","lamp="+String(value))
รูปที่ 9 แก้ไขคำสั่งที่ใช้ในฟิลด์ ONSTOP ACTION ของ Lamp Slider

ซึ่งจะส่งข้อความ lamp=value โดย value คือค่าตำแหน่งของ slider ในทำนองเดียวกัน ในฟิลด์ ONSTOP ACTION ของ Valve slider ใส่คำสั่ง


netpie["igrdatasource"].publish("@msg/cmd","valve="+String(value))

เพื่อส่งข้อความ valve=value

สำหรับ Fertilizer toggle ใส่คำสั่ง


netpie["igrdatasource"].publish("@msg/cmd","fertilizer=on")
netpie["igrdatasource"].publish("@msg/cmd","fertilizer=off")

ในฟิลด์ ONTOGGLEON ACTION และ ONTOGGLEOFF ACTION ตามลำดับ ดังในรูปที่ 10

รูปที่ 10 การตั้งค่าในฟิลด์ ONTOGGLEON ACTION, ONTOGGLEOFF ACTION ของ Fertilizer toggle

เมื่อโปรแกรมทางด้าน NodeMCU ทำงานอยู่และเชื่อมต่อกับ NETPIE 2020 ทดสอบโดยเลื่อน Lamp slider จะปรากฏข้อความบน Serial Monitor ดังในรูปที่ 11 แสดงว่าฟังก์ชัน callback() ได้รับข้อความและส่งต่อให้กับ cmdInt() เพื่อแปลคำสั่งและปรับความสว่างของ LED 10 มม. สีส้มที่จำลองหลอดไฟ (รูปที่ 6) การปรับค่าของ Valve slider จะให้ผลในลักษณะเดียวกัน จำลองโดย LED สีเขียวบนบอร์ด

รูปที่ 11 ข้อความบน Serial Monitor เมื่อปรับ Lamp slider

เมื่อทดลองคลิกปุ่ม Fertilizer toggle จะเห็นข้อความบน Serial Monitor ดังในรูปที่ 12 และ LED สีแดง (รูปที่ 8) ติดสว่าง

รูปที่ 12 ข้อความบน Serial Monitor เมื่อคลิกปุ่ม Fertilizer toggle

สรุป

ในบทความนี้อธิบายวิธีการสั่งงานอุปกรณ์ไอโอทีบนแพลตฟอร์ม NETPIE 2020 ซึ่งจะใช้วิธีการพับลิชโดยหัวข้อที่ขึ้นต้นด้วย @msg (เป็นชื่อหัวข้อที่ตั้งไว้สำหรับส่งข้อความ ผู้เขียนได้ทดลองตั้งชื่อหัวข้อเป็นอย่างอื่นเช่น @cmd พบว่าถึงแม้อุปกรณ์จะ subscribe หัวข้อนี้ NETPIE 2020 ก็จะไม่รู้จักและไม่สามารถรับส่งข้อความได้) วิธีการในตัวอย่างบนเว็บ netpie.io จะแยกคำสั่งโดยคำที่ตามหลัง @msg คั่นโดย / เช่น @msg/led ซึ่งจะเหมาะสมหากชุดคำสั่งมีไม่มาก แต่สำหรับนักพัฒนาที่เคยใช้รูปแบบคำสั่งอื่นมาก่อนก็สามารถใช้โค้ดเดิมร่วมกับ NETPIE 2020 ได้โดยตั้งชื่อหัวข้อเดียว และปรับส่วนข้อความที่ส่งให้สอดคล้องกับรูปคำสั่งที่เราใช้ ที่ผู้เขียนนิยมใช้คือรูป command=parameter เช่น lamp=10 ดังนั้นในตัวอย่างจะตั้งหัวข้อคำสั่งชื่อ @msg/cmd และ Lamp slider จะพับลิชในหัวข้อนี้โดยข้อความ lamp=value ซึ่งจะถูกแปลคำสั่งโดยฟังก์ชัน cmdInt() ที่ใช้โค้ดเดิม โดยวิธีการนี้จะช่วยลดการแก้ไขโปรแกรมจาก NETPIE 2015 (หรือแพลตฟอร์มอื่น) มาใช้งานบน NETPIE 2020

รวมโปรแกรมที่ใช้ในบทความ NETPIE2020cmd.zip

No comments:

Post a Comment

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

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