← Back to list

ลองใช้ M5Stack Tab5 ด้วย MicroPython + LVGL: ตอน 2 สร้าง GUI ด้วย LVGL

เนื้อหาตอนที่ 2 จะมาเล่าการทำ GUI ของ M5Stack Tab5 โดยเขียนโค้ด MicroPython เรียกใช้ API ของโมดูล LVGL ที่รวมมาด้วยกับเฟิร์มแวร์ UIFlow 2.0…

Supachai Vorapojpisut · 2025-09-24 10:16 · 2 claps · 5.0 min read
#lvgl #micropython #m5stack #esp32
Open on Medium ↗

ลองใช้ M5Stack Tab5 ด้วย MicroPython + LVGL: ตอน 2 สร้าง GUI ด้วย LVGL

เนื้อหาตอนที่ 2 จะมาเล่าการทำ GUI ของ M5Stack Tab5 โดยเขียนโค้ด MicroPython เรียกใช้ API ของโมดูล LVGL ที่รวมมาด้วยกับเฟิร์มแวร์ UIFlow 2.0 ใครที่ยังไม่ได้ลงเฟิร์มแวร์ สามารถย้อนกลับไปอ่านบทความตอนแรกที่แนะนำการติดตั้งเฟิร์มแวร์และ tool ได้

หน้าเว็บของ UIFlow 2.0

หน้าเว็บของ UIFlow 2.0

การสร้างโค้ด MicroPython สามารถใช้ web IDE ของ UIFlow 2.0 ที่ทาง M5Stack เตรียมไว้สำหรับทำ rapid prototype ได้ ผมแนะนำวิธีนี้เพราะจะเห็น รูปทรง/สี/ขนาด/font ของ UI component ต่างๆได้ก่อน ซึ่งจะช่วยให้เราประเมิน look-and-feel ของ GUI ได้ล่วงหน้า นอกจากนี้ ตัว web IDE ของ UIFlow 2.0 ยังรองรับการสร้าง LVGL page ทำให้เราสามารถออกแบบ layer ของ GUI แบบหลายหน้าจอได้ ทำให้การออกแบบ GUI ของการทำงานที่ซับซ้อนทำได้ง่ายขึ้น

บทความนี้จะแนะนำการพัฒนาโค้ดตัวอย่างของ M5Stack Tab5 เชื่อมต่อกับโมดูล Unit Weight-I2C ของ M5Stack ที่เป็น STM32 รวมกับ HX711 สำหรับใช้กับ load cell เพื่อชั่งน้ำหนัก โดยเชื่อมต่อผ่าน I2C ทางพอร์ท A ของ M5Stack Tab5 ได้เลย ตัว load cell ใช้รุ่น 0–20 kg ที่ซื้อจาก ThaiEasyElec ที่มีอยู่ในห้องแลป การออกแบบจะเน้น 2 ฟีเจอร์คือ การวัดและแสดงน้ำหนัก และส่วนการ calibrate การชั่งน้ำหนัก ข้อมูลของน้ำหนักจะรายงานผ่าน WiFi ด้วยโพรโทคอล MQTT ไปที่ public broker เช่น EMQX

ระบบต้นแบบ

ระบบต้นแบบ

ขั้นแรก ลอง draft ส่วน GUI

ส่วนออกแบบ GUI ของ UIFlow 2.0 มีตัวเลือกไลบรารีอยู่ 2 โมดูล คือ

  • M5GFX ซึ่งเป็น API สไตล์เก่าของ M5Stack มี UI component อยู่จำกัด เช่น label, rect, circle, image
  • M5UI เป็น API ที่พัฒนาต่อยอดจาก LVGL 9.3 โดยจะรองรับ UI component ที่หลากหลาย เช่น label, button, switch, checkbox ฯลฯ แต่ยังไม่ครบ component ทั้งหมดของ LVGL

บทความนี้จะใช้ไลบรารี M5UI โดยเข้าสู่ส่วนออกแบบ UI ด้วยการคลิกที่สัญลักษณ์ <ปากกา> จากนั้นจึงเป็นการลาก UI component จากทางซ้ายมาใส่ในพื้นที่ของหน้าจอ แล้วจึงปรับ UI component นั้นด้วยการป้อนค่า property ทางฝั่งขวา

ส่วนออกแบบ UI แบบ drag-and-drop ของ UIFlow 2.0

ส่วนออกแบบ UI แบบ drag-and-drop ของ UIFlow 2.0

ในกรณีที่ต้องการ UI แบบหลายหน้าจอ เราสามารถเพิ่ม page ด้วยการคลิกที่ปุ่ม Page: page0 แล้วจึงเพิ่ม page หลังจากเพิ่ม page ใหม่ ซึ่งจะเป็นหน้าจอว่าง เราจะสามารถลาก UI component มาใส่ การสลับ page จะสามารถเรียกใช้คำสั่งของ LVGL ในส่วนโค้ด

หน้าจอของระบบสาธิตจะแบ่งออกเป็น 2 page โดย page แรกจะเป็นหน้าจอหลัก ซึ่งจะอ่านค่าจากโมดูล Unit Weight-I2C แปลงค่าดิจิทัลเป็นน้ำหนัก และแสดงผล โดยจะมีปุ่มกด 2 ปุ่มสำหรับ reset และ calibrate การกำหนด property สำหรับแต่ละ UI component มีเฉพาะตัวเลือกพื้นฐาน เช่น ตำแหน่ง สี และ font ที่ใช้ โดย font ที่รวมมาด้วยในเฟิร์มแวร์คือ Montserrat ขนาดตั้งแต่ 14 จนถึง 48 pixel ซึ่งค่อนข้างจะเล็กหน่อยสำหรับหน้าจอความละเอียด 1280 × 720 pixel

UI สำหรับหน้าจอหลัก

UI สำหรับหน้าจอหลัก

สำหรับ page ที่สองจะเป็นหน้าจอ calibration ซึ่งมีส่วนการแสดงค่าดิจิทัลที่อ่านค่าได้ ปุ่มกดสำหรับตั้งค่า offset และปุ่มกดสำหรับอัพเดทค่า slope การกำหนดชื่อให้แต่ละ UI component ต้องไม่ซ้ำซ้อนกับ page แรก เพราะตัวแปรจะถูกสร้างขึ้นตามชื่อที่กำหนดให้แต่ละ UI component

ขั้นสอง การเขียนโค้ด

หลังจากกำหนด look-and-feel ของ GUI แล้ว เราสามารถเลือกพัฒนาซอฟต์แวร์ได้ 2 รูปแบบคือ การใช้ graphical programming สไตล์ Blockly หรือการ export เป็นโค้ด MicroPython บทความนี้จะเลือกรูปแบบ MicroPython เพราะเปิดให้สามารถเพิ่มความสามารถให้กับส่วน GUI ด้วยการเรียกใช้ API ของ LVGL ในการปรับแต่ง property ต่างๆของแต่ละ UI component รวมถึงเรียกใช้ฟังก์ชันต่างๆของไลบรารีที่เติมให้กับ MicroPython ได้

หน้าจอของ web IDE ของ UIFlow 2.0 แสดง Blockly UI และโค้ด MicroPython

หน้าจอของ web IDE ของ UIFlow 2.0 แสดง Blockly UI และโค้ด MicroPython

ส่วนโค้ด MicroPython ที่สร้างขึ้นจาก web IDE ของ UIFlow 2.0 จะเป็นโค้ดแบบ structure programming ไม่มีการสร้าง class ตามสไตล์ OOP โครงสร้างของโค้ดจะเลียนแบบ Arduino โดยประกอบด้วย 2 ฟังก์ชันหลัก คือ setup() สำหรับเตรียมการส่วน HW/SW และ loop() สำหรับการทำงานตามวงรอบ กลุ่มของตัวแปรของ UI component ต่างๆ จะอาศัยการประกาศ global เพื่อให้เรียกใช้ภายในฟังก์ชันได้ สำหรับคนที่มีประสบการณ์เขียนโค้ด OOP การมีโค้ดที่หุ้มส่วน UI component เป็น property ของ class ที่แยกไปตาม page ต่างๆ น่าจะเป็นตัวเลือกที่ดีกว่ามาก

import os, sys, io
import M5
from M5 import *
import m5ui
import lvgl as lv

page0 = None
page1 = None
weight_label = None
weight0_label = None
reset_btn = None
cal_btn = None
weight_value = None
weight0_value = None
digital_label = None
offset_label = None
digital_value = None
cal_weight_label = None
cal_weight_value = None
offset_value = None
offset_button = None
cal_button = None

def setup():
  global page0, page1, weight_label, weight0_label, reset_btn, cal_btn, weight_value, weight0_value, digital_label, offset_label, digital_value, cal_weight_label, cal_weight_value, offset_value, offset_button, cal_button

  M5.begin()
  Widgets.setRotation(3)
  m5ui.init()
  page0 = m5ui.M5Page(bg_c=0xeeeeee)
  page1 = m5ui.M5Page(bg_c=0xcccccc)
  weight_label = m5ui.M5Label("Weight", x=105, y=100, text_c=0x000000, bg_c=0xffffff, bg_opa=0, font=lv.font_montserrat_48, parent=page0)
  weight0_label = m5ui.M5Label("Start weight", x=100, y=250, text_c=0x000000, bg_c=0xffffff, bg_opa=0, font=lv.font_montserrat_48, parent=page0)
  reset_btn = m5ui.M5Button(text="RESET", x=300, y=500, bg_c=0x2196f3, text_c=0xffffff, font=lv.font_montserrat_48, parent=page0)
  cal_btn = m5ui.M5Button(text="CALIBRATE", x=700, y=500, bg_c=0x2196f3, text_c=0xffffff, font=lv.font_montserrat_48, parent=page0)
  weight_value = m5ui.M5Label("0 g", x=700, y=100, text_c=0x00ffff, bg_c=0xaaaaaa, bg_opa=255, font=lv.font_montserrat_48, parent=page0)
  weight0_value = m5ui.M5Label("0 g", x=700, y=250, text_c=0x00ffff, bg_c=0xaaaaaa, bg_opa=255, font=lv.font_montserrat_48, parent=page0)
  digital_label = m5ui.M5Label("Digital value:", x=100, y=100, text_c=0x000000, bg_c=0xffffff, bg_opa=0, font=lv.font_montserrat_48, parent=page1)
  offset_label = m5ui.M5Label("Offset value:", x=100, y=250, text_c=0x000000, bg_c=0xffffff, bg_opa=0, font=lv.font_montserrat_48, parent=page1)
  digital_value = m5ui.M5Label("0", x=700, y=100, text_c=0xffff00, bg_c=0x999999, bg_opa=255, font=lv.font_montserrat_48, parent=page1)
  cal_weight_label = m5ui.M5Label("Calibrated value:", x=100, y=400, text_c=0x000000, bg_c=0xffffff, bg_opa=0, font=lv.font_montserrat_48, parent=page1)
  cal_weight_value = m5ui.M5Label("0 g", x=700, y=400, text_c=0xffff00, bg_c=0x999999, bg_opa=255, font=lv.font_montserrat_48, parent=page1)
  offset_value = m5ui.M5Label("0", x=700, y=250, text_c=0xffff00, bg_c=0x999999, bg_opa=255, font=lv.font_montserrat_48, parent=page1)
  offset_button = m5ui.M5Button(text="OFFSET", x=250, y=550, bg_c=0x2196f3, text_c=0xffffff, font=lv.font_montserrat_48, parent=page1)
  cal_button = m5ui.M5Button(text="CALIBRATE", x=700, y=552, bg_c=0x2196f3, text_c=0xffffff, font=lv.font_montserrat_48, parent=page1)

  page0.screen_load()

def loop():
  global page0, page1, weight_label, weight0_label, reset_btn, cal_btn, weight_value, weight0_value, digital_label, offset_label, digital_value, cal_weight_label, cal_weight_value, offset_value, offset_button, cal_button
  M5.update()

if __name__ == '__main__':
  try:
    setup()
    while True:
      loop()
  except (Exception, KeyboardInterrupt) as e:
    try:
      m5ui.deinit()
      from utility import print_error_msg
      print_error_msg(e)
    except ImportError:
      print("please update to latest firmware")

โค้ด MicroPython + LVGL ที่สร้างจาก web IDE ของ UIFlow 2.0 จะมีข้อดีที่ส่วนโค้ดสำหรับการเริ่มต้น LVGL การจัดการหน่วยความจำ และการส่งข้อมูลไปยังส่วนแสดงผล จะถูกดำเนินการด้วยคำสั่งเดียวคือ m5ui.init() การเขียนโค้ดของเราจึงเจาะจงไปที่พฤติกรรมของ UI component ที่เป็นระดับ application ได้เลย แต่ก็เป็นข้อเสียว่าจะไม่ portable ไปยังบอร์ดไมโครคอนโทรลเลอร์อื่นๆ แน่นอนว่าประเด็น vendor lock-in อาจจะดูเป็นข้อเสียในระยะยาว แต่เมื่อเทียบกับการเขียนโค้ด LVGL ด้วยภาษา C/C++ การเขียนโค้ด MicroPython + web IDE ของ UIFlow 2.0 จะมีข้อได้เปรียบในแง่ rapid prototyping

การเขียนโค้ด LVGL ด้วย MicroPython จะง่ายกว่าภาษา C/C++ ค่อนข้างมาก เนื่องจากการผูกคำสั่งกับ object ของ UI component แต่ปัญหาใหญ่คือ เอกสารของ LVGL 9.x ไม่มีเนื้อหาที่อธิบาย API รวมถึงโค้ดตัวอย่างสำหรับ MicroPython ทำให้ต้องเดาคำสั่ง (เอกสาร LVGL 8.x มีตัวอย่างของ MicroPython แต่มี API พอสมควรที่เปลี่ยนชื่อ) หรือศึกษาจากโค้ดตัวอย่างของ M5UI บน github ผมจึงขอใช้โจทย์ของการพัฒนาอุปกรณ์ชั่งน้ำหนักแบบ IoT เพื่ออธิบายเป็นโค้ดตัวอย่างสำหรับหน้าที่ต่างๆของเฟิร์มแวร์

ส่วนแรกที่จะอธิบายเป็นโค้ดตัวอย่างคือ การใช้ WiFi ซึ่งหน่วยประมวลผล ESP32-P4 จะต้องเชื่อมต่อกับโมดูลสื่อสารที่เป็น ESP32-C6 เฟิร์มแวร์ของ UIFlow 2.0 จะหุ้มการทำงานของโมดูล ESP32-C6 ไว้แล้ว จึงสามารถเขียนโค้ดเหมือน MicroPython ด้วยการเรียกใช้ API ของโมดูล network

import network

def setup():
  ...
  sta_if = network.WLAN(network.WLAN.IF_STA)
  sta_if.active(True)
  sta_if.connect('WIFI_SSID', 'WIFI_PASSWD')
  while not sta_if.isconnected():
    time.sleep_ms(100)
    print('.')
  print(sta_if.ipconfig('addr4')) 

โค้ดถัดมาคือ การเชื่อมต่อ MQTT broker โดยใช้โมดูล umqtt ที่เป็นไลบรารีเสริมของ MicroPython การเขียนโค้ดสามารถศึกษาได้จากตัวอย่างของ MicroPython ที่เชื่อมต่อ MQTT

from umqtt.simple import MQTTClient
import ujson

def mqtt_msg_cb(topic, msg):
  print(topic, msg)

def setup():
  ...
  global mqtt_client
  mqtt_client = MQTTClient('MQTT_CLIENT_ID', 'broker.emqx.io', 1883)
  mqtt_client.set_callback(mqtt_msg_cb)
  mqtt_client.connect()
  mqtt_client.subscribe('MQTT_SUB_TOPIC')

def loop():
  ...
  global mqtt_client
  msg = {'value': 0}
  mqtt_client.publish('MQTT_PUB_TOPIC', ujson.dumps(msg))
  mqtt_client.check_msg()
  time.sleep(1)

ผลของการส่งข้อมูลไป MQTT broker ของ EMQX

ผลของการส่งข้อมูลไป MQTT broker ของ EMQX

เฟิร์มแวร์ UIFlow 2.0 มีไลบรารีสำหรับเชื่อมต่อกับฮาร์ดแวร์ของค่าย M5Stack มาด้วย ทำให้การเขียนโค้ดอ่านค่าจาก Unit Weight-I2C ทำได้ง่าย โดยศึกษาได้จากโค้ดตัวอย่างบน github จุดที่ต้องระวังคือ ขา I2C ที่เชื่อมต่อกับพอร์ท A จะแตกต่างไปตามฮาร์ดแวร์ เช่น M5Stack Tab5 จะใช้ขา 54 สำหรับ SCL และขา 53 สำหรับ SDA จึงต้องตั้งค่าของขา I2C ก่อนเรียกใช้งานไลบรารีของฮาร์ดแวร์

from machine import I2C, Pin
from unit import WeightI2CUnit

def setup():
  ...
  global weight_sensor
  i2c = I2C(0, scl=Pin(54), sda=Pin(53), freq=100000)
  weight_sensor = WeightI2CUnit(i2c, 0x26)

def loop():
  ...
  global weight_sensor
  print(weight_sensor.get_adc_raw)

หลังจากส่วน WiFi/MQTT และเซ็นเซอร์ทำงานแล้ว การเขียนโค้ด UI จะเริ่มจากการผูก logic ของหน้าจอ โดยผูกส่วนโค้ดที่เป็น callback กับ UI component ที่เป็นปุ่มกดของหน้าจอทั้งสอง

def main_reset_btn_cb(event_struct):
  event = event_struct.code
  if event == lv.EVENT.PRESSED:
    print('Main: RESET button')

def setup():
  ...
  reset_btn.add_event_cb(main_reset_btn_cb, lv.EVENT.ALL, None)

การเขียนโค้ดเพื่อจัดการ page และ UI component จะเรียกใช้ method ของ object ที่เกี่ยวข้อง เช่น การสลับ page1 เป็นหน้าจอจะใช้คำสั่ง page1.screen_load() ส่วนการอัพเดทข้อความจะใช้คำสั่ง digital_value.set_text()

def main_cal_btn_cb(event_struct):
  global page1
  event = event_struct.code
  if event == lv.EVENT.PRESSED:
    page1.screen_load()

def loop():
  ...
  digital_value.set_text(str(weight_sensor.get_adc_raw))  

ส่วนโค้ดสุดท้ายคือ state machine แบบง่าย สำหรับสอบเทียบน้ำหนัก และจดจำว่าหน้าจอปัจจุบันคือ page ไหน เพื่อให้อัพเดทค่าไปยัง UI component ที่สอดคล้อง ทั้งนี้ ค่าน้ำหนักสามารถสอบเทียบแบบง่ายด้วยการหักค่า ADC ที่รายงานจาก Unit Weight-I2C แล้วคำนวณ slope จากน้ำหนักที่ทราบค่า (ตอนทดลองโค้ดใช้ขวดน้ำ 600 cc)

การทำงานของโค้ดตัวอย่างที่รายงานค่าน้ำหนักไป MQTT broker

การทำงานของโค้ดตัวอย่างที่รายงานค่าน้ำหนักไป MQTT broker

終わりに (ในตอนท้าย)

เนื้อหาตอนนี้อยากชี้ให้เห็นว่าการเขียนโค้ดสร้าง GUI ด้วย LVGL แบบ rapid prototype ในกรณีของ M5Stack Tab5 ทำได้ง่ายมาก หากเข้าใจและเลือกใช้เครื่องมือให้เหมาะสม แต่โค้ดนี้เขียนแบบเร็วๆภายใน 1 วัน เลยยังไม่ค่อยเสถียรเท่าไร รวมทั้งปัญหาจุกจิกหลายอย่าง เช่น ต้องสอบเทียบใหม่ทุกครั้งหลังจากรีเซ็ตหรือปิดเครื่อง ซึ่งหากไปเรียกใช้ API เพื่อบันทึกค่า adc_offset และ adc_slope ในหน่วยความจำแบบ NVS ของ ESP32 ก็จะแก้ปัญหานี้ได้ สำหรับเนื้อหาตอนถัดไปของ M5Stack Tab5 คงขอดูจาก feedback ของผู้อ่านก่อน


메타데이터
post_id
e6bed9a19b43
slug
ลองใช้-m5stack-tab5-ด้วย-micropython-lvgl-ตอน-2-สร้าง-gui-ด้วย-lvgl-e6bed9a19b43
url
https://medium.com/@vsupacha-90388/%E0%B8%A5%E0%B8%AD%E0%B8%87%E0%B9%83%E0%B8%8A%E0%B9%89-m5stack-tab5-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-micropython-lvgl-%E0%B8%95%E0%B8%AD%E0%B8%99-2-%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-gui-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-lvgl-e6bed9a19b43
canonical_url
https://medium.com/@vsupacha-90388/%E0%B8%A5%E0%B8%AD%E0%B8%87%E0%B9%83%E0%B8%8A%E0%B9%89-m5stack-tab5-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-micropython-lvgl-%E0%B8%95%E0%B8%AD%E0%B8%99-2-%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-gui-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-lvgl-e6bed9a19b43
author_url
https://medium.com/@vsupacha-90388
status
ok
fetched_at
2026-06-20 20:29:01