สร้าง Web Server ด้วย MicroPython บน ESP32 ตอนที่ 1

ในบทความนี้ผมได้ใช้งาน Board จาก Ayarafun/LamLoei Node32s และ Espino32 ซึ่งเป็นของที่ผลิตในไทย มาใช้ในการทดสอบนะครับ
จะอนุมานว่า 1)ติดตั้ง adafruit-ampy และ 2) ทำการ Flash Firmware สำหรับ ESP32 เรียบร้อยแล้ว
เป้าหมายของในตอนนี้คือให้ทราบการติดตั้ง picoweb ซึ่งเป็น web framework ในภาษา MicroPython

1. เชื่อมต่อกับ Internet

เริ่มต้นการสร้าง function ขึ้นมาเพื่อเชื่อมต่อกับ Router ก่อน



def connect_to_router():
   import network

   ssid = 'YOUR SSID'
   password = 'YOUR SSID PASSWORD'

   sta_if = network.WLAN(network.STA_IF)
   ap_if = network.WLAN(network.AP_IF)

   sta_if.active(True)
   ap_if.active(False)

   sta_if.connect(ssid,password)



คำอธิบาย

1. เริ่มต้นด้วยการนำ network module เข้ามา
2. กำหนดชื่อ  SSID และ PASSWORD
3. สร้างตัวแปร sta_if เพื่อเชื่อมกับ wifi module บนบอร์ด  ด้วย WLAN ซึ่งเป็น network interface driver  ในที่นี้กำหนดให้อยู่ใน mode ของ station หรือ client
4. สร้างตัวแปร ap_if เพื่อเชื่อมกับ wifi module บนบอร์ด  ด้วย WLAN ซึ่งเป็น network interface driver  ในที่นี้กำหนดให้อยู่ใน mode ของ ap  ซึ่งสามารถปล่อยสัญญาณ Wifi ได้
5. ทำการยุติการทำงานใน ap mode และเริ่มต้นการทำงานของ station mode
6. เริ่มการเชื่อมต่อไปยัง Router

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

def connect_to_router():
   import network
   import time

   ssid = 'YOUR SSID'
   password = 'YOUR SSID PASSWORD'
   counter = 0
   sta_if = network.WLAN(network.STA_IF)
   ap_if = network.WLAN(network.AP_IF)

   sta_if.active(True)
   ap_if.active(False)
   sta_if.connect(ssid,password) 
   while not sta_if.isconnected() and counter < 10 :
       counter += 1
       time.sleep(1)
   if sta_if.isconnected() :
        print("Connected with {}".format(sta_if.ifconfig()[0]))
   else :
        print("Connection fail !")
        

เรียกใช้งาน

>>> connect_to_router()


2. ติดตั้ง picoweb

picoweb เป็น micro web frame เหมือนกับ Flask หรือ Bottle ซึ่งไม่ใช่ full stack web framework แต่ทำงานในสภาพแวดล้อมของ MicroPython (อ้างอิงจากข้อมูลของผู้พัฒนา picoweb ต้องการ RAM Size ขนาด 64K ในตอนเริ่มต้นการใช้งาน ดังนั้นอาจจะใช้งานกับ ESP8266 ไม่ได้) picoweb ต้องการใช้ dependencies สามตัวคือ uasyncio , pkg_resources และ  utemplate 

ท่านสามารถทำการติดตั้ง picoweb ได้สองวิธี คือ  แบบ Manual คือการทำสำเนา code จากเว็บผู้พัฒนามาวางไว้บนบอร์ดโดยตรง ซึ่งจะต้องทราบโครงสร้างของ package ก็จะออกจะยุ่งยากสักหน่อย และการใช้ utility ชื่อ upip ในบทความจะเลือกใช้ upip ครับ


import upip

upip.install('picoweb')


รอจนการติดตั้งสำเร็จ แล้วสำรวจดูผลของการติดตั้ง

import os

os.listdir()


จะพบว่ามีการสร้าง drirectory ชื่อ lib ขึ้นมา หากเข้าไปดูข้างในก็จะพบ package ที่ถูก download มาคือ uasyncio, logging.py, picoweb และ pkg_resources.py ยังขาดแต่ utemplate ซึ่งเราก็สามารถใช้ upip ได้เช่นกัน

upip.install('utemplate')

ท่านสามารถตรวจสอบ package ที่สามารถติดตั้งด้วย upip ได้จาก https://pypi.python.org/pypi?%3Aaction=search&term=micropython&submit=search

3. ปรับแต่ง picoweb


จากการทดสอบส่วนตัวพบว่า picoweb จะมอง content-type ของไฟล์ที่ส่งมายัง web browser เป็น "text/html" และจะต้องปะ header มาด้วยทุกครั้ง หากเราไม่ได้ใช้การแยกไฟล์ CSS และ Javascript ออกจากตัว HTML file ก็จะไม่มีปัญหา (นอกจากขนาดไฟล์ที่อาจใหญ่เกินไป) แต่หากเราใช้การแยกให้ download แยกกันก็จะเป็นประเด็นและพบ error ขึ้นที่ web browser ครับ หากเข้าไปดูใน source code ของ __init__.py ของ picoweb ในบรรทัดที่ 207 -209 จะเป็นดังนี้

with pkg_resources.resource_stream(self.pkg, fname) as f:
      yield from start_response(writer, content_type)
      yield from sendstream(writer, f)

ผมได้เติม code เข้าไปดังนี้

types = ["text/javascript","text/css"]
with pkg_resources.resource_stream(self.pkg, fname) as f:
     if content_type in types :
          await sendstream(writer, f)
     else :
          yield from start_response(writer, content_type)
          yield from sendstream(writer, f)
ทั้งนี้ก็เพื่อไม่ให้ picoweb ทำการปะ header เข้าไปใน javascript และ css ไฟล์ ผลก็ไม่พบ error ปรากฏบน web browser ครับ

4. Hello World
มาถึงขั้นตอนการสร้าง Web Server กัน เริ่มต้นด้วยการสร้างแสดง web page ที่มีประโยค "Hello World ! " กันตามธรรมเนียม  ก่อนอื่นต้องเข้าใจรูปแบบการสร้างโปรแกรม Web Server ในแบบของ picoweb ก่อน

เริ่มต้นการนำเข้า picoweb module

import picoweb


สร้าง instance ของ WebApp Class โดยส่งค่า __name__ ซึ่งเป็นชื่อของ module

app = picoweb.WebApp(__name__)


ต่อไปเป็นการกำหนด end point ของ request ที่เข้ามา โดยจะใช้ function route() ของ WebApp คอยรับ url ที่เรียกเข้ามาจาก client ผ่าน web browser  ในตัวอย่างนี้จะกำหนด url ที่จะทำ route คือ "/" ซึ่งหมายถึง landing page หรือ หรือหน้าแรก เทียบกับวิธีการใน Apache Web Server แล้วก็คือการ route ไปหา index.html นั่นเอง

@app.route("/")
def index(req,res):
     pass

เครื่องหมาย @ ข้างหน้า app.route("/") นั้นคือการทำ decoration ในภาษา Python ซึ่งก็คือการขยายการทำงานของ function ให้ไปทางที่ต้องการ ในตัวอย่างนี้เป็นการกำหนดให้มีการทำงานตามชุดคำสั่งใน function index(req,res) เมื่อมี url ที่มีค่า "/" เข้ามาที่ app.rount()

จะสังเกตุว่า function index() นั้นต้องการ parameter สองตัวคือ req และ res ตัวแรกเป็น instance ของ Class HTTPRequest และตัวที่สองคือ StreamWriter มีหน้าที่ในการส่งข้อมูลไปยัง client

กิจกรรมภายใน function index() จะมีแค่สองอย่างคือ เตรียมการส่งข้อมูลไปยัง client  แล้วทำการส่งข้อมูลออกไป  โดยข้อมูลที่ว่าคือ HTML Tag

<h2>Hello World !</h2>


ชุดคำสั่งภายใน function index()

@app.route("/")
def index(req,res):
     await picoweb.start_response(res)
     await res.awrite("<h2>Hello World !</h2>")



จากชุดคำสั่งข้างบนจะพบว่ามี keyword เพิ่มมาอีกคือ await หากไปเทียบกับตัวอย่างของ picoweb แล้วจะใช้ yield from ซึ่งมีความหมายเหมือนกัน ต่างกันตรงที่ await ใช้กับ Python 3.5 ส่วน yield from ใช้กับรุ่นก่อนหน้า

คำสั่งสุดท้ายในวันนี้คือ app.run() ซึ่งเป็นการสั่งให้ Web Server (ซึ่งอยู่ในตัวแปร app) เริ่มทำงาน



app.run(debug=True, host = "0.0.0.0",port=8080)


ชุดคำสั่งนี้บอกให้ Web Server ทำงาน โดยใช้ IP address ปัจจุบัน (0.0.0.0 แทน IP Address ในขณะนั้น) ที่ port เบอร์ 8080 (default คือ 8081)  ที่นี้มาดูชุดคำสั่งเต็ม ๆ


import picoweb

app = picoweb.WebApp(__name__)

@app.route("/")
def index(req,res):
     await picoweb.start_response(res)
     await res.awrite("<h2>Hello World !</h2>")

app.run(debug=True, host = "0.0.0.0",port=8080)



หลังการเขียนชุดคำสั่งบรรทัดสุดท้ายจบลง ESP32 ก็พร้อมจะทำงานเป็น Web Server แล้ว มาดูที่ Web Browser

สำเร็จ ! ตอนนี้ ESP32 ก็กลายเป็น Web Server ไปแล้วด้วย MicroPython ครับ

ความคิดเห็น