ESP32 Programming with M5Stack Core

แนะนำการอุปกรณ์สมองกลฝังตัวที่เรียกว่า M5Stack M5 Core ซึ่งใช้ตัวประมวลผลเป็นชิป ESP32 และเขียนโปรแกรมได้โดยใช้ภาษา MicroPython และใช้เฟิร์มแวร์ของ M5Stack UIFlow

แนะนำอุปกรณ์ ESP32-M5Stack

M5Stack.com (Shenzhen, China) ได้พัฒนาและจำหน่ายโมดูลหรืออุปกรณ์อิเล็กทรอนิกส์ที่ใช้ Espressif ESP32 เป็นตัวประมวลผลหลัก แบ่งออกได้เป็นหลายรุ่น เช่น M5 Core (BASIC / GRAY / FIRE), M5Stick-C และ M5 ATOM รองรับการเขียนโปรแกรมด้วย Arduino และ MicroPython

อุปกรณ์ที่ได้นำมาใช้งานสำหรับการลองเขียนโค้ดไมโครไพธอน คือ M5 Core ซึ่งมีให้เลือก เช่น M5 BASIC / GRAY (ไม่มี PSRAM มีแต่ SPI Flash ขนาด 4MB) และ M5 FIRE (มี PSRAM ขนาด 4MB เพิ่มมาให้) เป็นต้น

ข้อมูลเชิงเทคนิคเกี่ยวกับ M5 BASIC

  • ESP32 with WiFi / Bluetooth capability (2.4 GHz)

  • 4MB of SPI Flash

  • Micro-SD Card Slot (up to 16GB Storage)

  • Type-C USB Port / USB-to-Serial Converter

  • 320x240 2.0" TFT Color Display (ILI9342C driver)

  • 3 User Buttons (A, B, C) + 1 Reset Button

  • Small 1W speaker

  • 150 mAh LiPo Battery

  • Grove Port / I2C Connector

ข้อมูลเชิงเทคนิคเกี่ยวกับ M5 GRAY

  • An upgrade version of M5 BASIC

  • I2C IMU Sensor: MPU9250 or MPU6886 + BMM150

  • Analog MEMS Microphone BSE3729

ข้อมูลเชิงเทคนิคเกี่ยวกับ M5 FIRE

  • An upgrade version of M5 GRAY (with 16MB SPI Flash + 4MB PSRAM)

  • Type-C USB port / USB-to-Serial converter

  • Grove Ports (I2C + I/O + UART)

  • 2-inch, 320x240 TFT LCD (ILI9342C driver)

  • Speaker 1W-0928

  • MEMS Analog BSE3729 Microphone

  • 3 Custom Buttons

  • SK6812 3535 RGB LED x 10

  • IMU Sensor: MPU9250 or BMM150 + MPU6886

  • 550 mAh @ 3.7V LiPo Battery + IP5306 (I2C)

โมดูล M5 Basic และ M5 Gray จะมีบอร์ดฐานปิดด้านล่าง เรียกว่า Base CORE Bottom ซึ่งมีแบตเตอรี LiPo (110 mAh @ 3.7V) อยู่ภายใน และมีคอนเนกเตอร์ที่เรียกว่า M-Bus และทั้งสี่ด้านมี Pin Headers เอาไว้สำหรับต่อสายเชื่อมต่อกับอุปกรณ์หรือวงจรภายนอกได้

บอร์ดฐานยังมีอีกหลายแบบ จำแนกตามกลุ่มได้แก่ โมดูลสื่อสาร (Communication Modules) เช่น LoRa/LoRaWAN, SIM800L, GPS Receiver, W5500 (Ethernet) เป็นต้น โมดูลแบตเตอรีที่มีความจุมากกว่า 100 mAh หรือโมดูลขยายพอร์ตสำหรับต่อวงจร (Expansion Modules) และโมดูลสำหรับขับมอเตอร์ไฟฟ้ากระแสตรง (Drive Modules) เป็นต้น

ข้อสังเกต: M5 FIRE มีราคาสูงกว่ารุ่นก่อนหน้า และมีหน่วยความจำ SPI Flash เพิ่มจาก 4MB เป็น 16MB และที่สำคัญคือ มีการเพิ่มไอซีหน่วยความจำ SPI RAM / PSRAM ที่มีความจุ 4MB ดังนั้นเหมาะกับการเขียนโค้ดด้วยไมโครไพธอน

ในปัจจุบันทางบริษัท M5Stack ได้ออกแบบและพัฒนาอุปกรณ์ที่ใช้ตัวประมวลผล ESP32 ออกมาอีกหลายรูปแบบ แต่สำหรับเนื้อหาในเอกสารนี้ เราจะกล่าวถึงและใช้งานเฉพาะ M5Stack Core

ULFlow IDE: Block-based Programming

เพื่อสนับสนุนการเขียนโปรแกรม บริษัท M5Stack ได้พัฒนาซอฟต์แวร์ UIFlow (http://flow.m5stack.com) ซึ่งเป็น Web-based IDE และใช้งานแบบออนไลน์ (มีเวอร์ชัน UIFlow Desktop IDE ให้ดาวน์โหลดและติดตั้งใช้งานแบบ Offline ได้เช่นกัน)

UIFlow = “A Web IoT programming platform using Blockly+MicroPython”

ซอฟต์แวร์ดังกล่าวใช้สำหรับการเขียนโปรแกรมด้วยวิธีการต่อบล็อกตามรูปแบบของ Google Blockly และสามารถใช้ในการออกแบบกราฟิกโดยใช้ UI Designer หรือจะเปลี่ยนโหมดไปใช้ภาษาไมโครไพธอนในการเขียนโค้ดก็ได้

สำหรับผู้เริ่มต้น การใช้บล็อกคำสั่งของ UIFlow แล้วแปลงให้เป็นโค้ด อาจเป็นวิธีหนึ่งที่ช่วยในการศึกษา และทำให้เห็นตัวอย่างการใช้คำสั่งได้ง่ายขึ้น นอกเหนือจากการศึกษาจากเอกสารเกี่ยวกับ API ของ UIFlow และไลบรารีที่เกี่ยวข้อง

การติดตั้ง UIFlow Firmware โดยใช้โปรแกรม M5Burner

‍‍การติดตั้งไฟล์เฟิร์มแวร์ไปยัง M5 Core สามารถใช้โปรแกรม M5 Burner หรือ UI Flow Desktop IDE (มีให้ดาวน์โหลดสำหรับ Windows 64-bit, Linux และ Mac OS X) ในบทความนี้จะสาธิตวิธีการใช้โปรแกรมชื่อ esptool ที่ทำงานโดยใช้ Python 3 เป็นอีกหนึ่งวิธี

เราจะไม่ใช้ไฟล์สำหรับติดตั้งเฟิร์มแวร์ MicroPython (ESP32 Port) แต่จะใช้ M5-UIFlow Firmware (ลองใช้เวอร์ชัน 1.5.4 — 2020.06.05) ที่บริษัท M5Stack ได้พัฒนาเอาไว้ เนื่องจากได้รวมไลบรารีอย่างเช่น UIFlow-Code ไว้แล้ว

ดาวนโหลดไฟล์ M5Burner.zip (ในกรณีนี้ ใช้สำหรับ Windows 64-bit) จากนั้นทำคำสั่ง Unzip จะได้ไดเรกทอรีใหม่ชื่อ M5Burner ให้ดับเบิลคลิกที่ไฟล์ M5Burner.exe เพื่อเรียกใช้งาน

ข้อสังเกต: ถ้าลองใช้เวอร์ชัน เช่น v1.5.4 สำหรับ M5 Core Basic / Gray ให้เลือกไฟล์ UIFlow-v1.5.4-en.bin แต่ถ้าใช้สำหรับ M5 Core Fire ให้เลือกไฟล์ UIFlow-v1.5.4-fire.bin

เมื่อได้ดาวน์โหลดไฟล์ .bin มาแล้ว จะมีปุ่ม “Burn” ปรากฏ จากนั้นให้เสียบสาย USB เชื่อมต่อกับอุปกรณ์ M5 Core และทำขั้นตอนอัปโหลดไฟล์เฟิร์มแวร์

การติดตั้ง UIFlow Firmware โดยใช้โปรแกรม esptool.py

จากขั้นตอนที่แล้ว จะสังเกตได้ว่า M5Burner ได้ใช้คำสั่ง esptool สำหรับทำขั้นตอน Burn Firmware และอ่านไฟล์ .bin จากไดเรกทอรี M5Burner\packages\fw\core

ดังนั้นถ้าจะลองใช้คำสั่ง esptool ดูบ้าง ให้แน่ใจว่า ในเครื่องคอมพิวเตอร์มี Python 3 ไว้พร้อมใช้งานแล้ว (ถ้าไม่มีให้ติดตั้งก่อน) จากนั้นให้ติดตั้ง esptool โดยใช้คำสั่ง pip

python3 -m pip install -U esptool

เสียบสาย USB เชื่อมต่อกับอุปกรณ์ แล้วลองทำคำสั่ง (ลองอ่านหมายเลข MAC Address ของอุปกรณ์)

python3 -m esptool read_mac

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

python3 -m esptool --chip esp32 erase_flash

จากนั้นทำคำสั่งต่อไปนี้ เพื่อเขียนไฟล์เฟิร์มแวร์ไปยังอุปกรณ์ M5 BASIC / GRAY:

python3 -m esptool --chip esp32 --baud 921600 ^
  --before default_reset --after no_reset ^
  write_flash -z --flash_mode dio --flash_freq 80m ^
  --flash_size detect 0x1000 UIFlow-v1.5.4-en.bin

การตั้งค่า Wi-Fi และทำงานในโหมด Internet

ในกรณีที่ต้องการลองใช้ UIFlow แบบออนไลน์ จะต้องตั้งค่า Wi-Fi (SSID และ Password) ให้กับอุปกรณ์ก่อน เพื่อให้สามารถเชื่อมต่อไปยังอินเทอร์เน็ตได้

การตั้งค่า Wi-Fi ให้อุปกรณ์ ทำได้โดยเปิดใช้งานโหมด Wi-Fi AP โดยไปที่เมนู Setup > “Wi-Fi via AP” (สามารถใช้ปุ่มกดที่มีอยู่สามปุ่มของอุปกรณ์เลือกเมนูได้) ซึ่งจะเห็น SSID ของอุปกรณ์ที่ปรากฏชื่อเป็น M5-XXXX (X=แทนตัวเลขฐานสิบหก) เป็นแบบ Open ไม่มีรหัสผ่าน

เมื่ออุปกรณ์อยู่ในโหมด Wi-Fi AP แล้ว ให้คอมพิวเตอร์ของผู้ใช้เชื่อมต่อกับระบบ Wi-Fi ดังกล่าว จากนั้นเปิด Web browser ไปที่ 192.168.4.1 จะมีหน้าเว็บให้ผู้ใช้เลือก WiFi SSID และรหัสผ่านที่ต้องการเลือกใช้งาน เมื่อเชื่อมต่อได้แล้ว อุปกรณ์จะรีเซตตัวเอง

ขั้นตอนถัดไปคือ ทำให้อุปกรณ์ทำงานในโหมด Internet โดยเลือกเมนู Switch Mode > Internet Mode จากนั้นกดปุ่มรีเซตของอุปกรณ์อีกครั้ง เมื่อระบบเริ่มทำงานใหม่ จะพยายามเชื่อมต่อ Wi-Fi จากนั้นจะเชื่อมต่อไปยัง M5 UIFlow Server (flow.m5stack.com) ตามลำดับ

การใช้งาน UIFlow IDE แบบออนไลน์

การใช้ซอฟต์แวร์ UIFlow IDE จะต้องมีการระบุ API Key ซึ่งเป็นเลขฐานสิบหกที่มีจำนวน 8 หลัก (8-Digit Hex String) และมีค่าแตกต่างกันไปสำหรับแต่ละอุปกรณ์ เราจะทราบได้ก็เมื่อได้ติดตั้ง M5 Firmware ลงในอุปกรณ์ M5 Core แล้ว

เมื่อระบบเริ่มทำงานเข้าสู่ Internet Mode หรือ USB Mode จะมีการแสดงค่าดังกล่าวบนจอ LCD ของอุปกรณ์ ค่า API KEY นี้จะต้องนำไปใช้กับ UIFlow IDE จึงจะรันโค้ดกับอุปกรณ์ที่เชื่อมต่อได้

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

ข้อคิดเห็นของผู้เขียน: จากการทดลองใช้ UIFlow IDE (แบบออนไลน์) การเขียนโค้ดและทดสอบการทำงานร่วมกับอุปกรณ์จริง ก็ทำได้สะดวก แต่มีข้อจำกัดอยู่บ้าง เช่น การตรวจสอบ Syntax ของโค้ด และการดีบักหรือแสดงเอาต์พุตการทำงานของโค้ด เช่น ด้วยวิธีการใช้คำสั่ง print() ไม่สามารถทำได้โดยตรง ยกเว้นว่า จะใช้คำสั่ง lcd.print() มาเป็นตัวช่วย

อีกประเด็นหนึ่งคือ เมื่อเกิดความผิดพลาดระหว่างที่รันโค้ด เช่น เกิด Runtime Error ก็จะมีข้อความแจ้งปัญหา (Error Message) สั้น ๆ แสดงบนหน้าจอ LCD ของอุปกรณ์ และจะไม่เหมือนกรณีที่ใช้ MicroPython REPL

การทำงานในโหมด USB และใช้งานร่วมกับ Thonny Python IDE

ถ้าจะเขียนโค้ดแบบ Offline เช่น ใช้ UIFlow Desktop IDE (ดาวน์โหลดไฟล์ติดตั้งได้จาก https://m5stack.com/pages/download) หรือโปรแกรมอื่นเช่น Thonny IDE หรือ Mu Editor จะต้องทำให้อุปกรณ์อยู่ในโหมด USB

เมื่อกดปุ่มรีเซตที่ตัวอุปกรณ์ จะปรากฏเมนูบนหน้าจอ LCD จากนั้นให้เลือกโหมด Setup เลือกไปที่ Switch Mode แล้วเปลี่ยนจาก APP Mode เป็น USB Mode

ถ้าเราตั้งค่าดังกล่าวแล้ว เมื่อกดปุ่มรีเซตของตัวอุปกรณ์อีกครั้ง ระบบจะเริ่มทำงานโดยเข้าสู่โหมด USB

ถัดไปเป็นการลองใช้โปรแกรม Thonny IDE สำหรับระบบปฏิบัติการ Windows (ถ้ายังไม่มี ให้ดาวน์โหลดไฟล์และติดตั้งใช้งานก่อน)

เสียบสาย USB เชื่อมต่อกับอุปกรณ์ M5 Core จากนั้นเลือกเมนูคำสั่ง Run > Select Interpreter ให้เป็น MicroPython (generic) และระบุพอร์ตของอุปกรณ์ M5 Core ที่กำลังเชื่อมต่ออยู่

เมื่อเราเชื่อมต่อกับอุปกรณ์ได้แล้ว ให้กดปุ่ม “Stop (Stop/Restart) ของโปรแกรม Thonny IDE เพื่อหยุดการทำงานของโปรแกรมของอุปกรณ์ที่กำลังทำงานอยู่ในขณะนั้น เราจะเข้าโหมด REPL และปรากฎสัญลักษณ์ >>> สำหรับรับคำสั่งถัดไป

ปัญหาที่อาจพบ: บ่อยครั้งที่เมื่อเชื่อมต่อครั้งแรกจาก Thonny IDE ผ่านทาง USB-to-Serial อาจไม่มีการตอบสนองจากบอร์ด และจะต้องลองหลายครั้งจึงจะสำเร็จ

ในมุมมอง “Files” เราจะเห็นรายการไฟล์และไดเรกทอรีย่อยภายใน Flash Storage ของอุปกรณ์ เช่น ไดเรกทอรี flash ที่มีไฟล์ boot.py และ main.py เป็นต้น

เมื่อเปิดระบบ เช่น หลังจากการกดปุ่มรีเซต และระบบเริ่มทำงาน ก็จะเรียกไฟล์ boot.py เพื่อทำคำสั่งที่อยู่ภายในเป็นลำดับแรก ถัดจากนั้นจะตรวจสอบดูว่า มีไฟล์ main.py หรือไม่ ถ้ามี ก็ให้เรียกไฟล์ดังกล่าวให้ทำงานเป็นลำดับถัดไป แต่ถ้า M5 Core อยู่ในโหมด USB จะไม่มีการรันคำสั่งในไฟล์ main.py

ตัวอย่างโค้ดที่ 1

ตัวอย่างโค้ดแรก แสดงข้อความบนกลางจอ LCD มีพื้นหลัง (Background Color) เป็นสีดำ แล้วเปลี่ยนสีของข้อความตามค่าสี (ค่าคงที่สำหรับการเลือกใช้สีแบบ RGB) ที่ใส่ไว้ในอาร์เรย์

ให้บันทึกโค้ดลงในไฟล์ /flash/temp.py ใน Flash Storage ของอุปกรณ์ สามารถทำได้โดยใช้ Thonny IDE ในมุมมอง View > Files แล้วกดปุ่ม “Run” (Run Current Script)

import utime as time
from m5stack import lcd

# get screen dimension (width x height) in pixels
scr_w, scr_h = lcd.screensize()
print( 'LCD screen: {}x{}'.format(scr_w,scr_h) )
# fill color as black
lcd.fill( lcd.BLACK )
# set font for text display
lcd.font( lcd.FONT_DejaVu24 )
# list of colors to be used
COLORS = [
    lcd.BLUE, lcd.GREEN, lcd.RED,
    lcd.CYAN, lcd.GREEN, lcd.LIGHTGREY,
    lcd.MAGENTA, lcd.ORANGE, lcd.PINK,
    lcd.PURPLE, lcd.WHITE,
    lcd.YELLOW, lcd.GREENYELLOW ]

try:
    # press Ctrl+C to terminate
    while True:
        for color in COLORS:
            # set text color
            lcd.setTextColor(color)
            # get font size (width, height)
            font_w, font_h = lcd.fontSize()
            # show a text message centered on screen
            xpos, ypos = (lcd.CENTER, (scr_h - font_h)//2)
            lcd.print('Hello MicroPython', xpos, ypos )
            time.sleep_ms(1000)
except KeyboardInterrupt:
    pass
finally:
    lcd.clear()
    print('Done')

ตัวอย่างโค้ดที่ 2

โค้ดตัวอย่างนี้ แสดงข้อความบนกลางจอ LCD แล้วเปลี่ยนฟอนต์ (Font) ของระบบที่ใช้ในการแสดงข้อความตามรายการที่กำหนดไว้ในอาร์เรย์

import utime as time
from m5stack import lcd

# get screen dimension (width x height) in pixels
scr_w, scr_h = lcd.screensize()
print( 'LCD screen: {}x{}'.format(scr_w,scr_h) )
# fill color as black
lcd.fill( lcd.BLACK )
# list of fonts to be used
FONTS = [
    lcd.FONT_DefaultSmall, lcd.FONT_Default,
    lcd.FONT_Small, lcd.FONT_Comic, 
    lcd.FONT_DejaVu18, lcd.FONT_DejaVu24,
    lcd.FONT_DejaVu40, lcd.FONT_DejaVu56,
    lcd.FONT_DejaVu72, lcd.FONT_Minya, 
    lcd.FONT_Tooney ]

try:
    # press Ctrl+C to terminate
    lcd.setTextColor( lcd.GREENYELLOW )
    while True:
        for font in FONTS:
            # clear the LCD screen
            lcd.clear()
            # set text font
            lcd.font(font)
            # show a text message centered on screen
            font_w, font_h = lcd.fontSize()
            ypos = (scr_h - font_h)//2
            lcd.print( 'Hello', lcd.CENTER, ypos )
            time.sleep_ms(1000)
except KeyboardInterrupt:
    pass
finally:
    print('Done')
    lcd.clear()

โค้ดตัวอย่างที่ 3

โค้ดตัวอย่างถัดไป สาธิตการตรวจสอบว่า มีการกดปุ่ม A, B หรือ C หรือไม่ โดยเราสามารถใช้ btnA, btnB และ btnC จากไลบรารี m5stack สำหรับปุ่มกดทั้งสาม และมีคำสั่งให้ใช้ เช่น wasPressed() และ wasReleased() โดยจะต้องระบุฟังก์ชันที่ทำหน้าที่เป็น Callback Function เมื่อเกิดเหตุการณ์ดังกล่าว (มีเกิดการกดปุ่มหรือปล่อยปุ่มในแต่ละครั้ง)

from m5stack import lcd, btnA, btnB, btnC

lcd.clear()
lcd.fill( lcd.BLACK )
lcd.setTextColor( lcd.GREENYELLOW )
lcd.font( lcd.FONT_DejaVu24 )

font_w, font_h = lcd.fontSize()
ypos = 120 - font_h//2

lcd.print('Please press a button', lcd.CENTER, ypos)

text = 'Button {} was pressed!'

def btnA_wasPressed():
    lcd.clear()
    lcd.print( text.format('A'), lcd.CENTER, ypos )
    
def btnB_wasPressed():
    lcd.clear()
    lcd.print( text.format('B'), lcd.CENTER, ypos )
    
def btnC_wasPressed():
    lcd.clear()
    lcd.print( text.format('C'), lcd.CENTER, ypos )
    
btnA.wasPressed( btnA_wasPressed )
btnB.wasPressed( btnB_wasPressed )
btnC.wasPressed( btnC_wasPressed )

try:
    while True:
        pass
except KeyboardInterrupt:
    pass
finally:
    lcd.clear()
    print('Done')

โค้ดตัวอย่างที่ 4

โค้ดตัวอย่างถัดไป สาธิตการใช้คำสั่งเกี่ยวกับปุ่มกดทั้งสาม เช่น การตรวจสอบว่ามีการกดปุ่ม (เช่น ปุ่ม btnA) มีระยะเวลาอย่างน้อยในการกดค้างไว้ การกดปุ่มแบบดับเบิลคลิก (เช่น ปุ่ม btnB) หรือมีการกดปุ่มแล้วปล่อยแล้วหรือไม่ (เช่น ปุ่ม btnC) เป็นต้น

from m5stack import lcd, btnA, btnB, btnC

lcd.clear()
lcd.fill( lcd.BLACK )
lcd.setTextColor( lcd.GREENYELLOW )
lcd.font( lcd.FONT_DejaVu24 )

font_w, font_h = lcd.fontSize()
ypos = 120 - font_h//2
lcd.print('Please press a button', lcd.CENTER, ypos)

def btnA_callback():
    lcd.clear()
    lcd.print( 'A: long pressed', lcd.CENTER, ypos )
   
def btnB_callback():
    lcd.clear()
    lcd.print( 'B: double pressed', lcd.CENTER, ypos )
    
def btnC_callback():
    lcd.clear()
    lcd.print( 'C: released', lcd.CENTER, ypos )
    
btnA.restart()
btnB.restart()
btnC.restart()
btnA.pressFor( 2.0, btnA_callback )
btnB.wasDoublePress( btnB_callback )
btnC.wasReleased( btnC_callback )

try:
    while True:
        pass
except KeyboardInterrupt:
    pass
finally:
    lcd.clear()
    print('Done')

โค้ดตัวอย่างที่ 5

ตัวอย่างถัดไปเป็นการสร้างเสียง โดยใช้สัญญาณ PWM เป็นเอาต์พุต และสร้างอ็อบเจกต์จากคลาส machine.PWM และเลือกใช้ขา GPIO25 (ต่อกับวงจรลำโพงเสียงภายในของ M5 Core Basic) กำหนดค่าความถี่ได้ (หน่วยเป็น Hz) และค่า Duty Cycle ในช่วง 0 ถึง 100 (เลือกค่าให้น้อยลง เพื่อลดระดับความดังของสัญญาณเสียง)

ในตัวอย่างนี้ เราสร้างอ็อบเจกต์จากคลาส machine.Timer (เลือกใช้หมายเลข 0..4 สำหรับ Hardware Timer ของ ESP32) มาใช้ในโหมด Oneshot เพื่อกำหนดระยะเวลาในการสร้างสัญญาณเสียงแล้วปิดเสียงหลังจากนั้น

import machine
import utime as time

# global variable
pwm = None

def timer_cb(t):
    global pwm
    if pwm:
       pwm.duty(0) # setPWM duty cycle to 0
       pwm.deinit()
       pwm = None
    t.deinit()
    
def tone( freq_hz, duration_ms=100 ):
    global pwm
    while pwm is not None:
        time.sleep_ms(10)
    t = machine.Timer(4)  # use timer 4
    pwm = machine.PWM(25) # use GPIO25 pin for PWM
    pwm.freq(freq_hz)     # set PWM frequency (Hz)
    pwm.duty(5.0)         # set PWM duty cycle (0..100)
    t.init( period=duration_ms, mode=t.ONE_SHOT, callback=timer_cb )
    
tone(500,200) # 500Hz, 200 msec
tone(700,200) # 700Hz, 200 msec
tone(900,200) # 900Hz, 200 msec

หรือเราจะใช้คำสั่ง speaker แทนก็ได้ เช่น

from m5stack import *
from m5ui import *
from uiflow import *

speaker.volume(1) # set speaker volume
speaker.tone(900, 200) # make a tone (freq. 900Hz, duration 200 ms)

ในกรณีที่เราต้องการหยุดการทำงานของอุปกรณ์เพื่อประหยัดพลังงาน เราสามารถใช้คำสั่งเพื่อทำให้ ESP32 เข้าสู่โหมด Deep Sleep เช่น เมื่อกดปุ่ม C ของอุปกรณ์ ก็สามารถใช้คำสั่งดังนี้

if btnC.wasPressed():
    machine.deepsleep( 7*24*60*60*1000 ) # sleep duration in msec

โค้ดตัวอย่างที่ 6

สร้างรูปกราฟิกที่เป็นวงกลม (Circle) แบบสุ่มพิกัด (x,y) ขนาดรัศมี (r) โดยใช้หน่วยเป็นพิกเซล และสี RGB ของเส้นวงกลม (ไม่ระบายสีภายใน) สร้างทั้งหมด 100 วงกลม คำสั่งที่ใช้วาดวงกลมคือ lcd.circle() โดยที่ lcd เป็นส่วนหนึ่งของโมดูล m5stack

from m5stack import lcd
import utime as time
import urandom as random

def rand_color():
    r = random.randint(50,255)
    g = (random.randint(50,255) << 8) 
    b = (random.randint(50,255) << 16)
    return (r | g | b)
    
lcd.clear( 0x111111 )
w,h = lcd.screensize()
for i in range(100):
    r = random.randint(10, 20)
    x = random.randint(r, w-r)
    y = random.randint(r, h-r)
    lcd.circle(x, y, r, rand_color())

เราสามารถเปลี่ยนมาใช้ M5Circle() ซึ่งอยู่ในโมดุล m5ui เพื่อสร้างอ็อปเจกต์ (Object) เป็นรูปวงกลมแทนการใช้คำสั่ง lcd.circle()

from m5stack import *
from m5ui import *
from uiflow import *
import utime as time
import urandom as random

def rand_color():
    r = random.randint(50,255)
    g = (random.randint(50,255) << 8) 
    b = (random.randint(50,255) << 16)
    return (r | g | b)
    
setScreenColor(0x111111)
lcd.clear()
w,h = lcd.screensize()
circles = []
for i in range(100):
    r = random.randint(10, 20)
    x = random.randint(r,w-r)
    y = random.randint(r,h-r)
    color = rand_color()
    circle = M5Circle(x, y, r, color)
    circles.append( circle )
    wait_ms(20)
    
while True: 
    i = random.randint( 0, len(circles)-1 )
    circles[i].show() # redraw on top
    wait_ms(100)

ลองเปลี่ยนเป็นรูปกราฟิกทรงสี่เหลี่ยม (Rectangle) แบบสุ่มพิกัดเริ่มต้น และสุ่มขนาดของสี่เหลี่ยม (ความกว้างและความยาว) โดยใช้วิธีการสร้างอ็อปเจกต์จาก M5Rect

from m5stack import *
from m5ui import *
from uiflow import *
import utime as time
import urandom as random

def rand_color():
    r = random.randint(0,255)
    g = (random.randint(0,255) << 8) 
    b = (random.randint(0,255) << 16)
    return (r | g | b)
  
setScreenColor(0x111111)
lcd.clear()
w,h = lcd.screensize()

for i in range(100):
    rw = random.randint(10,50) 
    rh = random.randint(10,50)
    x = random.randint(0,w-rw)
    y = random.randint(0,h-rh)
    color = rand_color()
    M5Rect( x, y, rw, rh, color, color )

โค้ดตัวอย่างที่ 7

ตัวอย่างถัดไปสาธิตการสุ่มเลขจำนวนเต็มในช่วง 0..200 จำนวน N=50 แล้วนำข้อมูลที่ได้มาแสดงเป็นกราฟแท่งตามลำดับของข้อมูลในอาร์เรย์

from m5stack import *
from m5ui import *
from uiflow import *
import utime as time
import urandom as random

lcd.clear(0x222222)
w,h = lcd.screensize()
N = 50
values = [ random.randint(1,200) for i in range(N) ]

xs = (w-4)//N
if xs < 1:
   xs = 1
x = (w - xs*N)//2
if x < 0:
    x = 0
y = (h-20)
for v in values:
    rw = 1 if xs < 2 else xs-2
    lcd.rect( x, y-v, rw, v, lcd.DARKGREEN, lcd.GREEN )
    x += xs

โปรเจกต์สาธิต: การเชื่อมต่อกับ ZigBee2MQTT SErver

ตัวอย่างที่ได้เลือกมาสาธิตการใช้งานคือ การเชื่อมต่อกับ MQTT Broker ที่ได้จากการติดตั้งโปรแกรม Mosquitto สำหรับบอร์ด Raspberry Pi และทำงานร่วมกับโปรแกรม Zigbee2mqtt ที่ทำหน้าที่เป็นตัวเชื่อมต่อ (Bridge) ระหว่างเครือข่ายไร้สาย ZigBee กับ MQTT Broker

ขั้นตอนการติดตั้งใช้งาน Zigbee2mqtt สำหรับบอร์ด Raspberry Pi ทางผู้พัฒนาได้เขียนอธิบายอย่างละเอียดแล้ว ดังนั้นศึกษาได้จาก https://www.zigbee2mqtt.io/getting_started/running_zigbee2mqtt.html

ในกรณีสาธิตนี้ เราจะใช้ M5 Core เชื่อมต่อไปยัง MQTT Broker พอร์ต 1883 ตามหมายเลข IP Address ของบอร์ด Rasbperry Pi ที่ได้นำมาใช้งาน และจะต้องมีการระบุชื่อผู้ใช้และรหัสผ่านสำหรับ MQTT User Authentication ด้วย (ในโค้ดตัวอย่างได้ตั้งค่าไว้เป็น zigbee2mqtt : zigbee2mqtt)

เมื่อเริ่มต้นการทำงาน อุปกรณ์ M5 Core จะเชื่อมต่อกับ Wi-Fi เข้าสู่ระบบเครือข่ายโดยอัตโนมัติ (ถ้าเคยได้ตั้งค่าใช้งานอย่างถูกต้องไว้แล้ว) และเชื่อมต่อกับ MQTT Broker ได้แล้ว จะทำหน้าที่คอยรับข้อความในหัวข้อ (Topic) ตามที่ระบุได้

เมื่อมีข้อความซึ่งอยู่ในรูปแบบของ JSON String ถูกส่งมาจาก MQTT Broker ด้วยรูปแบบการสื่อสาร (Protocol) ที่เรียกว่า MQTT ข้อมูลในข้อความที่ได้รับ จะถูกนำมาแสดงผลบนจอ LCD

ในกรณีตัวอย่างนี้เป็นข้อมูลของโมดูลเซ็นเซอร์ Xiaomi Mijia Temperature and Humidity Sensor ได้แก่ ค่าอุณหภูมิ ค่าความชื้นสัมพัทธ์ ระดับของแบตเตอรี่ที่เหลืออยู่ และแรงดันของแบตเตอรี่

from m5stack import *
from m5ui import *
from uiflow import *
import ujson as json
import wifiCfg
from m5mqtt import M5mqtt

# set the friendly_name of your Xiaomi Mijia device
friendly_name = '0x00158d00xxxxxxxx'
# set the IP address of your MQTT server
mqtt_host   = '192.168.x.x'
mqtt_port   = 1883
mqtt_user   = 'zigbee2mqtt'
mqtt_passwd = 'zigbee2mqtt'

setScreenColor(0x222222)
lcd.clear()
lcd.font(lcd.FONT_DejaVu24)

m5mqtt = M5mqtt('m5stack', mqtt_host, mqtt_port,
                mqtt_user, mqtt_passwd, 300 )
while True:
    if wifiCfg.wlan_sta.isconnected():
        lcd.print('Wi-Fi connected', lcd.CENTER, 60, lcd.GREENYELLOW)
        break
    else:
        lcd.print('Wi-Fi reconnecting', lcd.CENTER, 100, lcd.RED)
        wifiCfg.reconnect()
        wait_ms(1000)

lcd.font(lcd.FONT_DejaVu18)
lcd.print('Waiting for MQTT messages', lcd.CENTER, 150) 
wait_ms(1000)

def mqtt_sub_cb(topic_data):
    msg = topic_data.decode()
    data =  json.loads(msg)
    battery = data['battery']
    voltage = data['voltage']
    humid = data['humidity']
    temp = data['temperature']
    xpos = 20
    ypos = 10
    lcd.clear()
    lcd.font(lcd.FONT_DejaVu18)
    lcd.rect(0, 0, 320, 36, 0xafafaf, 0xafafaf)
    name = friendly_name[2:]
    lcd.print('Device: ' + name, xpos, ypos, lcd.CYAN)
    ypos += 50
  
    lcd.font(lcd.FONT_DejaVu18)
    lcd.print('Battery [%]', xpos, ypos, lcd.WHITE)
    lcd.font(lcd.FONT_DejaVu24)
    lcd.print(battery, xpos+200, ypos, lcd.GREENYELLOW)
    ypos += 40
  
    lcd.font(lcd.FONT_DejaVu18)
    lcd.print('Voltage [mV]', xpos, ypos, lcd.WHITE)
    lcd.font(lcd.FONT_DejaVu24)
    lcd.print(voltage, xpos+200, ypos, lcd.GREENYELLOW)
    ypos += 40
  
    lcd.font(lcd.FONT_DejaVu18)
    lcd.print('Temperature [C]', xpos, ypos, lcd.WHITE)
    lcd.font(lcd.FONT_DejaVu24)
    lcd.print('%.2f' % float(temp), xpos+200, ypos, lcd.GREENYELLOW)
    ypos += 40
  
    lcd.font(lcd.FONT_DejaVu18)
    lcd.print('Humidity [%RH]', xpos, ypos, lcd.WHITE)
    lcd.font(lcd.FONT_DejaVu24)
    lcd.print('%.2f' % float(humid), xpos+200, ypos, lcd.GREENYELLOW)
  
# subscribe messages on the specified topic
m5mqtt.subscribe( 'zigbee2mqtt/' + friendly_name, mqtt_sub_cb )
# start mqtt thread 
m5mqtt.start()
while True:
    wait_ms(100)

กล่าวสรุป

โดยสรุป การใช้งานซอฟต์แวร์ UIFlow / UIFlow-Desktop IDE และเฟิร์มแวร์ที่เกี่ยวข้องร่วมกับอุปกรณ์ M5 Core (ESP32) เพื่อเขียนโปรแกรมด้วยภาษาไมโครไพธอน ก็ถือว่าทำได้สะดวก และสามารถใช้ชุดคำสั่งหรือไลบรรีที่ทาง M5Stack ได้จัดทำไว้แล้ว แต่ก็มีข้อจำกัดอยู่บ้างในการใช้งาน จุดเด่นน่าจะเป็นตัวช่วยในการออกแบบ UI แบบกราฟิกที่แสดงผลบนจอ TFT-LCD ขนาด 320x240 พิกเซล เราสามารถนำไปประยุกต์ใช้งาน เช่น การสร้างอุปกรณ์ IoT ควบคุมและแสดงผลที่มีขนาดเล็กสำหรับผู้ใช้และเชื่อมต่อ Wi-Fi ได้

เผยแพร่ภายใต้ลิขสิทธิ์ Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)

Last updated