Arduino + ESP8266 กับการควบคุมหลอดไฟผ่านระบบอินทราเน็ต

เครื่องควบคุมหลอดไฟผ่าน Wifi

เครื่องควบคุมหลอดไฟผ่าน Wifi

อันนนี้เป็นส่วนหนึ่งของโปรเจค iHome App นะครับ ที่จะเปลี่ยนบ้านธรรมดาๆให้กลายเป็นบ้านแห่งอัตโนมัติ (Smart Home) โดยเริ่มต้นควบคุมหลอดไฟก่อน ในบล็อกนี้ก็จะนำวิธีการทำโดยละเอียด วิธีการทำ โค้ด และวงจร มาให้ได้ชมและลองทำตามดูครับ

ในตัวเครื่องที่ใช้ควบคุมนั้น จะประกอบไปด้วย ESP8266 , Arduino Pro Mini 328 – 3.3V/8MHz แหล่งจ่ายไฟ (แกะมาจากอะแดปเตอร์ชาร์จแบตเตอรี่มือถือ) จ่ายแรงดัน 5V กระแส 300mA และวงจร Solid State Relay ครับ

ราคาของ Arduino Pro Mini 328 – 3.3V/8MHz ตอนนี้ที่ ArduinoAll อยู่ที่ 150 บาท และ  โมดูล ESP8266 ก็ราคา 150 บาทเช่นกัน (ราคา ณ วันที่ 3/11/57) ส่วนวงจร Solid State Relay ผมสั่งมาจาก ES ครับ ส่วนกล่อง และอื่นๆ ก็ใส่ได้ตามสะดวกเลยครับ ราคาทั้งหมดทั้งสิ้นโดยรวมแล้ว ไม่เกิน 500 บาทครับ แต่มันจะเสียเงินเยอะตรงที่ลองผิดลองถูกจนผมทำโมดูล ESP8266 เสียไปแล้ว 2 ตัว ซึ่งนี่ละที่เรียกขาดทุนของจริง = =”

จากที่ทดสอบมา หาก ESP8266 นำไปใช้งานกับวงจรที่มีไฟ 5V ร่วมอยู่ด้วย จะทำให้มันเอ๋อๆ และทำงานผิดพลาดบ่อยครั้ง ผมจึงลองใช้ร่วมกับตัว Pro Mini 3.3V ดู แล้วพบว่ามันทำงานได้ดีมากๆ เบิร์นโค้ดลงไป ต่อวงจรบนโพรโทบอร์ดแล้วใช้งานได้เลย

เริ่มที่วงจร

วงจร Arduino + ESP8266

SSR - R Load

SSR Circuit

ในวงจรจะมี LED 4 ดวงไว้บอกสถานะ โดย LED D1 จะบอกว่ามีไฟจ่ายเข้าวงจรหรือไม่ LED D2 จะแสดงสว่างขึ้นหากมีความผิดพลาดเกิดขึ้นในการทำงาน LED D3 จะสว่างขึ้นเมื่อเครื่องพร้อมใช้งานแล้ว และ LED D4 จะแสดงสถานะว่าสั่งเปิด หรือปิดไฟอยู่

โดยปกติแล้วหลังจากเริ่มทำงาน จะมีไฟสีแดงติดประมาณ 1 – 2 ครั้ง แต่ละครั้ง ไม่เกิน 30 วินาที หากนานกว่านั้นแสดงว่ามีปัญหากับการเชื่อมต่อโมดูล ESP8266 ซึ่งต้องใช้ Debug Pin (Debug Tx, Debug Rx, Debug GND) ตรวจสอบว่าผิดพลาดส่วนใด

ตัว Arduino Pro mini 3.3V นั้น ใช้งานไอซีเรกูเลเตอร์เบอร์ MIC5205 รองรับการจ่ายแรงดันอินพุตที่ขา RAW ได้ตั้งแต่ 3.35V ไปจนถึง 12V และจ่ายกระแสได้สูงสุด 150mA ซึ่งเท่าที่ทดสอบสามารถนำไปจ่ายให้กับ ESP8266 ได้แบบสบายๆ

วงจร SSR นั้น ในตอนที่ทดสอบ หากใช้วงจรที่มี C ต่อร่วมอยู่ด้วย (วงจรที่ใช้ควบคุมโหลดที่เป็น L มีการเปลี่ยนแปลงเฟสมาเกี่ยวข้อง) หากมีลอจิก 0 จะมีแรงดันตกคร่อมที่โหลดประมาณ 100V – 150V ซึ่งไม่เหมาะสำหรับมาใช้ควบคุมโหลดที่เป็นหลอดไฟอย่างยิ่ง เนื่องจากหลอดไฟที่เป็นหลอดตะเกียบ สามารถทำงานที่แรงดัน 110V ได้ ดังนั้นหากป้อนลอจิก 0 ไฟจะไม่ดับ จะกระพริบหารติดแบบไม่สว่าง เมื่อลองใช้วงจรที่ใช้ R ตัวเดียวคั่นระหว่างออปโต้กับไตรแอด พบว่าเมื่อมีลอจิก 0 ไฟจะไปตกคร่อมโหลดแค่ 20 – 30V เท่านั้น (ใช้มิเตอร์เข็มวัดได้ 25V) ดังนั้นวงจรที่ใช้ R ตัวเดียวจึงเป็ยวงจรเดียวที่ดีที่สุดสำหรับการควบคุมหลอดไฟ ใช้งานแทนรีเลย์ซึ่งมีห้นาสัมผัสและเกิดเสียงในขณะเปลี่ยนโหมดการทำงาน

ต่อด้วยซอฟแวร์

เมื่อได้ตัว ESP8266 มาใหม่ๆ ต้องอัพเดทเฟิร์มแวร์ก่อน เพื่อให้รองรับกับโค้ดที่ผมใช้ในบล็อกนี้ วิธีการอัพเฟิร์มแวร์สามารถอ่านได้ที่ Update_Firmware_ESP8266.pdf ครับ (ขอบคุณเว็บ ThaiEasyElec สำหรับบทความนี้มากๆครับ) เมื่ออัพเดทเฟิร์มแวร์แล้ว อัพโค้ดด้ารล่างนี้ลง Arduino ไปได้เลย

#include <SoftwareSerial.h>
#define SSID  "Max Home"
#define PASS  "987654321"

String UID = "#001";

String Msg, Str = "OFF";
int LEDStatus = 6;
int LEDOnOff = 12;
int LEDError = 8;
int ch_pd = 9;
boolean LoopRuning = false; 

SoftwareSerial dbgSerial(10, 11); // RX, TX

void setup(){
  // Open serial communications and wait for port to open:
  pinMode(LEDStatus, OUTPUT);
  pinMode(LEDOnOff, OUTPUT);
  pinMode(LEDError, OUTPUT);
  pinMode(ch_pd, OUTPUT);
  digitalWrite(ch_pd, LOW);
  delay(500);
  digitalWrite(ch_pd, HIGH);

  delay(5000);

  Serial.begin(9600);

  while (!Serial){
    errorShow(); // wait for serial port to connect. Needed for Leonardo only
    delay(100);
  }

  errorOff();

  Serial.setTimeout(500);
  dbgSerial.begin(9600);  //can't be faster than 19200 for softserial

  delay(3000); 

  dbgSerial.print("Test ESP8266...");

  while (1){
    if (sendAndWait("AT","OK", 100)){
      errorOff();
      break;
    }else{
      dbgSerial.println("To Text");
      Serial.println("AT");
      delay(100);
      while (Serial.available()) {
        dbgSerial.write(Serial.read());
      }
      errorShow();
      delay(500);
    }
  }

  dbgSerial.println("OK");
  dbgSerial.println("Set mode");

  Serial.println("AT+CWMODE=1");
  delay(100);

  dbgSerial.println("Connect to router");
  while (1){
    if (connectWiFi(SSID,PASS)){
      errorOff();
      break;
    }else{
      dbgSerial.println("Connect to router error");
      errorShow();
      delay(500);
    }
  }

  dbgSerial.println("Set the multiple connection mode");
  while (1){
    if (sendAndWait("AT+CIPMUX=1","OK",300)){
      errorOff();
      break;
    }else{
      dbgSerial.println("AT+CIPMUX=1");
      Serial.println("AT");
      delay(300);
      while (Serial.available()) {
        dbgSerial.write(Serial.read());
      }
      errorShow();
      delay(500);
    }
  }

  dbgSerial.println("Create TCP Port 8000");
  while (1){
    if (createTCPPort("8000")){
      errorOff();
      break;
    }else{
      dbgSerial.println("Create TCP error");
      errorShow();
      delay(500);
    }
  }

  dbgSerial.println("ip address:");
  Serial.println("AT+CIFSR");
  delay(100);
  while (Serial.available()) {
    dbgSerial.write(Serial.read());
  }
  dbgSerial.println();
  dbgSerial.println("Start TCP Server");
  LoopRuning = true;
}

void loop() {
  if (LoopRuning == true){
    if (digitalRead(LEDStatus) == 0)
      digitalWrite(LEDStatus, HIGH);
    while (Serial.available() >0 ){
      char c = Serial.read();
      Msg += c;
      if (Msg.indexOf("OK") >= 0) {
        dbgSerial.println("TCP Server");
        dbgSerial.println("Msg = " + Msg);
        if (Msg.indexOf("+IPD,0,3:OFF") >= 0){
          dbgSerial.println("LED to off");
          digitalWrite(LEDOnOff, LOW);
        }else if (Msg.indexOf("+IPD,0,2:ON") >= 0){
          dbgSerial.println("LED to on");
          digitalWrite(LEDOnOff, HIGH);
        }else if (Msg.indexOf("+IPD,0,3:GET") >= 0){
          dbgSerial.println("get led");
          if (digitalRead(LEDOnOff) == 1)
            Str = "ON";
          else
            Str = "OFF";
          Received("0", Str);
        }else if (Msg.indexOf("+IPD,0,3:UID") >= 0){
          dbgSerial.println("get uid");
          Received("0", UID);
        }
        Msg = "";
      }
    }
  }else{
    errorShow();
    delay(10000);
  }
}

// Get the data from the WiFi module and send it to the debug serial port
boolean sendAndWait(String AT_Command, char *AT_Response, int wait){
  dbgSerial.print(AT_Command);
  Serial.println(AT_Command);
  delay(wait);
  while ( Serial.available() > 0 ) {
    //dbgSerial.write(Serial.read());
    if ( Serial.find(AT_Response)  ) {
      dbgSerial.print(" --> ");
      dbgSerial.println(AT_Response);
      return true;
    }
  }
  dbgSerial.println(" fail!");
  return false;
}

boolean connectWiFi(String NetworkSSID,String NetworkPASS){
  String cmd = "AT+CWJAP=\"";
  cmd += NetworkSSID;
  cmd += "\",\"";
  cmd += NetworkPASS;
  cmd += "\"";

  // dbgSerial.println(cmd);
  //sendAndWait(cmd,"OK",10);
  Serial.println(cmd);
  delay(100);
  while (Serial.available()) {
    dbgSerial.write(Serial.read());
    return true;
  }
  return false;
}

boolean createTCPPort(String Port){
  Serial.println("AT+CIPSERVER=1," + Port);
  delay(300);
  while (Serial.available()) {
    dbgSerial.write(Serial.read());
    if (Serial.find("OK") || Serial.find("no change"))
      return true;
    else
      return false;
  }
  return false;
}

boolean Received(String id, String Str){
  Serial.print("AT+CIPSEND=" + id + ",");
  Serial.println(Str.length());
  if (Serial.find(">")){
    return sendAndWait(Str,"SEND OK", 100);
  }
  return false;
}

void errorShow(){
  if (digitalRead(LEDError) == 0)
    digitalWrite(LEDError, HIGH);
}

void errorOff(){
  if (digitalRead(LEDError) == 1)
    digitalWrite(LEDError, LOW);
}

ขอบคุณเว็บไซต์ ayarafun.com สำหรับโค้ดบางส่วนครับ

SSID และ Pass อย่าลืมแก้ให้ตรงกับที่ตั้งไว้ใน Access Point ด้วยนะครับ เมื่อโปรแกรมเริ่มทำงานแล้ว จะค่อยๆทำคำสั่งไปทีละขั้นๆ ตามลำดับ หากคำสั่งใดส่งไปให้ ESP8266 แล้วไม่สำเร็จ ก็จะสั่งให้ LED Error ติดขึ้นมา แล้ววนลูปทำคำสั่งนั้นใหม่จนกว่าจะสำเร็จ เมื่อทำคำสั่งสำเร็จทั้งหมดแล้ว ก็สั่งให้ LDE OK ติดขึ้นมา เพื่อเป็นการบ่งบอกว่าพร้อมใช้งานแล้ว

เพิ่มเติม – SSID คือชื่อของ Access Point ที่ตั้งค่าไว้นะครับ ต้องกรอกเป็นชื่อแบบตรงกัน หากไม่ตรงกัน เช่น มีเว้นวรรคเกินมา ก็จะทำให้มันหา Access Point ไม่เจอ แล้วเชื่อมต่อไม่สำเร็จครับ

เมื่อมีการเชื่อมต่อ TCP เข้ามาแล้ว จะมีการส่งรายละเอียดไปให้ทาง Debug Pin แล้วทำการตรวจสอบว่าข้อความที่ได้รับมาใช่ OFF หรือไม่ หากใช่ สั่งให้ Pin 12 เป็นลอจิก 0 หากไม่ใช่ ให้ตรวจสอบว่าใช่ ON หรือไม่ ถ้าใช่ สั่งให้ Pin 12 เป็นลอจิก 0 หากข้อความเป็น GET ให้ส่งสถานะปัจจุบันกลับไป หากข้อความเป็น UID ให้ส่งค่า UID ที่ตั้งไว้กลับไป (ไว้เผื่อกรณีต้องการยืนยันว่าใช่เครื่องที่จะสั่งจริงๆหรือเปล่า ไว้ใช้เทียบคู่กับ IP)

เสียใจด้วย ESP8266 ไม่สามารถตั้ง Fix IP ได้

TP-Link DHCP Client List

TP-Link DHCP Client List

แต่ทำที่ Access Point (รุ่นใหม่ๆ) ได้ครับ ตัว Access Point ของผมเป็นของ TP-Link รุ่น TL-MR3420 สามารถทำ Fix IP ได้ครับ โดยคลิกไปที่เมนู DHCP เลือกเมนูย่อย DHCP Client List จากนั้นสังเกตุตรง Client Name หาแถวที่มีชื่อเป็น Unknown แล้วก๊อบ MAC Address เก็บไว้

หากหาไม่เจอ ตรวจสอบดูดีๆ ว่าตัวเครื่องทำงานได้ปกติ สามารถเชื่อมต่อกับ Access Point ได้แล้ว Debug Pin มีประโยชน์มากครับ

Add or Modify an Address Reservation Entry

Add or Modify an Address Reservation Entry

คลิกไปที่เมนูย่อย Address Reservation คลิกปุ่ม Add New… นำ MAC Address ที่ก๊อบไว้มาวางลงในช่อง แล้วเลือก IP ที่จะให้เป็นเมื่อมีการเชื่อมต่อเข้ามา ได้ในช่อง Reserved IP Address และช่อง Status หากเป็น Enablad อยู่แล้ว ก็อย่าไปแก้ไขครับ กดปุ่ม Save

Add or Modify an Address Reservation Entry

Add or Modify an Address Reservation Entry

ระบบจะพากลับมาหน้า Address Reservation แล้วคลิกตรง click here เพื่อ Reboot Access Point เพื่อให้การแก้ไขค่ามีผลทันที

หลังจาก Reboot เสร็จแล้ว ก็กด Restart Arduino ไปหนึ่งครั้ง เมื่อทำการเชื่อมต่อได้แล้ว IP ก็จะเป็นแบบที่เราตั้งค่าไว้

ทดสอบสั่งให้ทำงาน

Hercules TCP Client

Hercules TCP Client

โปรแกรมที่จะใช้งาน คือโปรแกรม Hercules สามารถดาว์โหลดได้ที่ hercules_3-2-8.exe เมื่อดาว์โหลดเสร็จแล้ว สามารถเปิดใช้งานโปรแกรมได้เลย ไม่ต้องติดตั้ง เมื่อเปิดโปรแกรมขึ้นมาแล้ว คลิกแท๊บ TCP Client กรอก IP และกรอก Port เป็น 8000 (ในโค้ดผมตั้งไว้เป็น 8000 หากตั้งเป็นอย่างอื่น ให้กรอกที่ตั้งไว้) จากนั้นคลิกปุ่ม Connect แล้วรอดูผล หากขึ้นว่า Connected to (IP) สีเขียวๆแสดงว่าเชื่อมต่อสำเร็จแล้ว ทดสอบส่งข้อความว่า GET จะได้สถานะกลับมา หากพึ่งใช้งานครั้งแรก จะได้ค่ากลับมาเป็น OFF (ค่าส่งไปเป็นสีชมพู ค่าส่งกลับเป็นสีดำ) จากนั้นจึงลองส่ง ON และ OFF ตามลำดับ เพื่อทดสอบว่าใช้งานได้ปกติหรือไม่ หากใช้งานได้ปกติทั้งหมดเป็นอันจบการทดสอบ

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

อย่างไรก็ตาม การเชื่อมต่อแบบ TCP เป็นการสื้อสารแบบ 2 ช่องทาง ดังนั้นหากมีฝั่งใดฝั่งหนึ่งไม่มีการส่งค่าไป หรือรับมา เกินเวลาที่กำหนด จะทำให้ตัดการเชื่อมต่ออัตโนมัติ และปิด Soccket หากต้องการส่งคำสั่งไป ต้องสั่งเชื่อมต่อใหม่เพื่อสั่งเปิด Socket ใหม่อีกครั้ง ก็จะทำให้สามารถส่งคำสั่งไปได้อีกครั้ง

เขียนโปรแกรมควบคุมหลอดไฟด้วยภาษา AutoIT

เนื่องจากการเชื่อมต่อแบบ TCP เป็นพื้นฐานการเชื่อมต่อเข้าสู่โลกอินเตอร์อยู่แล้ว ทำให้ทุกภาษารองรับการเชื่อมต่อแบบ TCP ทั้งหมด แต่อยู่ที่ว่าแต่ละภาษานั้นรองรับแค่ไหน ใช้งานสะดวกแค่ไหนมากกว่า

ผมใช้งานภาษา AutoIT เพราะมีความรู้ด้านภาษานี้เป็นอย่างดี จึงสามารถเขียนโปรแกรมออกมาได้อย่างรวดเร็ว ผู้อ่านสามารถก๊อบโค้ดไปใช้งานได้เลยครับ

#include <ButtonConstants.au3>
#include <EditConstants.au3>
#include <GUIConstantsEx.au3>
#include <GuiIPAddress.au3>
#include <StaticConstants.au3>
#include <WindowsConstants.au3>

$LED = False
$Connect = False
$socket = -1

#Region ### START Koda GUI section ### Form=C:\Users\Max\Documents\LED Control.kxf
$Form1 = GUICreate("LED Control", 331, 186, -1, -1)
$Group1 = GUICtrlCreateGroup("Connect", 8, 16, 313, 57)
$InputIP = _GUICtrlIpAddress_Create($Form1, 24, 40, 162, 21)
_GUICtrlIpAddress_Set($InputIP, "192.168.0.107")
$InputPort = GUICtrlCreateInput("8000", 198, 40, 41, 21, BitOR($GUI_SS_DEFAULT_INPUT,$ES_NUMBER))
$Label1 = GUICtrlCreateLabel(":", 190, 42, 7, 17)
$ButtonConnect = GUICtrlCreateButton("Connect", 248, 37, 59, 25)
GUICtrlCreateGroup("", -99, -99, 1, 1)
$ButtonON = GUICtrlCreateButton("LED ON", 40, 104, 107, 49)
$ButtonOFF = GUICtrlCreateButton("LED OFF", 184, 104, 107, 49)
GUISetState(@SW_SHOW)
#EndRegion ### END Koda GUI section ###

GUICtrlSetState($ButtonON, $GUI_DISABLE)
GUICtrlSetState($ButtonOFF, $GUI_DISABLE)

While 1
	$nMsg = GUIGetMsg()
	Switch $nMsg
		Case $GUI_EVENT_CLOSE
;~ 			TCPCloseSocket($socket)
			TCPShutdown()
			Exit
		Case $ButtonConnect
			if ($Connect == True) Then
				TCPCloseSocket($socket)
;~ 				TCPShutdown()
			Else
				TCPStartup()
			EndIf
			$sIP = _GUICtrlIpAddress_Get($InputIP)
			$sPort = GUICtrlRead($InputPort)
			TCPStartup()
			Local $socket = TCPConnect($sIP, $sPort)
			If $socket = -1 Then
				MsgBox(16, "Error", "Connent error!")
			Else
				TCPSend($socket, StringToBinary("GET", 4))
;~ 				TCPCloseSocket($socket)
				$LED = ""
				While ($LED = "")
					$LED = TCPRecv($socket, 2048)
				WEnd
;~ 				MsgBox(0, "", $LED);
				If $LED == "ON" Then
					GUICtrlSetState($ButtonON, $GUI_DISABLE)
					GUICtrlSetState($ButtonOFF, $GUI_ENABLE)
				Else
					GUICtrlSetState($ButtonON, $GUI_ENABLE)
					GUICtrlSetState($ButtonOFF, $GUI_DISABLE)
				EndIf
;~ 				GUICtrlSetState($ButtonConnect, $GUI_DISABLE)
				GUICtrlSetData($ButtonConnect, "Reconnect")
				GUICtrlSetState($InputIP, $GUI_DISABLE)
				GUICtrlSetState($InputPort, $GUI_DISABLE)
			EndIf
		Case $ButtonON
			SetLED("ON")
			GUICtrlSetState($ButtonON, $GUI_DISABLE)
			GUICtrlSetState($ButtonOFF, $GUI_ENABLE)
		Case $ButtonOFF
			SetLED("OFF")
			GUICtrlSetState($ButtonON, $GUI_ENABLE)
			GUICtrlSetState($ButtonOFF, $GUI_DISABLE)
	EndSwitch
WEnd

Func SetLED($St)
;~ 	Local $socket = TCPConnect($sIP, $sPort)
	TCPSend($socket, StringToBinary($St, 4))
;~ 	TCPCloseSocket($socket)
EndFunc

สามารถรัน แล้วกรอก IP กรอก Port กดปุ่ม Connect ได้เลย รอซักครู่ แล้วปุ่ม LED ON หรือ LED OFF จะสามารถกดได้ครับ แต่หากใช้ไปนานๆ จะทำให้ถูกตัดการเชื่อมต่ออัตโนมัติ ทำให้ต้องทำการเชื่อมต่อใหม่จึงจะสามารถควบคุมได้ (โค้ดด้านบนผมยังไม่ได้แก้ไขบัคนี้)

### จบ รอติดตามได้ครับ บล็อกหน้าจะเป็นการควบคุมหลอดไฟโดยใช้โมดูยอินทราเน็ตสำเร็จรูป ใช้งานง่ายกว่า ESP8266 เยอะ สั่งให้ทำงานได้เร็วกว่า แต่ต้องลากสายแลนไปด้วย ###

ลิขสิทธิ์ภาพ และเนื้อหาของบทความเป็นลิขสิทธิ์ของ Elec-Za.com เพียงผู้เดียว ไม่อนุญาตให้ก๊อบไปใช้งานในเว็บอื่น หรือนำเนื้อหาส่วนหนึ่งส่วนใดไปใช้เด็ดขาด

  • kaebmoo

    Thanks for this article.

  • tanawat panyalesattha

    มีคำถามครับ
    pin VCC ต่อมาจาก เรกกูเรเตอร์เบอร์ MIC5205 ใช่มั้ยคับ

  • นายช่่าง

    ขอบคุณน๊ครับบทความดีๆอยากเรียนถามหน่อยครับ
    ความหมาย
    arduino Pin10 –> Debug Rx
    arduino Pin11 –> Debug Tx ความหมาย Debug Rx Debug Tx และ Debug Ground ความหมายคื่ออะไร แล้ว
    hardware Debug Tx Rx Ground อยุ่ตรงใหนครับเป็น Hardware หรอครับ