ภาพพื้นหลังปกบทเรียน

บทเรียน 2.6 — ลงมือทำ: แผงควบคุมสัมผัสของเรา และ widget ขั้นต่อไป

สร้าง Touch UI ควบคุมฮาร์ดแวร์ · แผงควบคุมของทีมเราเอง: แตะบนจอแล้วไฟจริงติด

โมดูล 2 — จากจอสู่ฮาร์ดแวร์

ต่อจากบทเรียน 2.5 — event loop: แตะจอแล้วไฟจริงติด

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

MVP checkpoint — ผ่านชุดบทเรียนนี้เมื่อ

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

แปลเป็นสิ่งที่ตรวจได้จริง (ตรงกับหกช่องว่างใน s04_touch_panel.py):

  • [ ] จอมีสามแถว แถวละ ไฟสถานะ + ปุ่ม "เปิด" + ปุ่ม "ปิด" · การ์ดขวามีตัวเลข "ติดอยู่ n จาก 3 ดวง" + ปุ่ม "เปิดทั้งหมด" + "ปิดทั้งหมด" · บรรทัดสถานะข้างหัวเรื่อง
  • [ ] แตะ "เปิด" แถวไหน หลอดไฟจริงบนบอร์ด ติดสีนั้น แตะ "ปิด" แล้วดับ — กด "เปิด" ซ้ำสิบครั้ง ผลเท่ากดครั้งเดียว
  • [ ] "เปิดทั้งหมด" ติดครบสามสี · "ปิดทั้งหมด" เปิดกล่องยืนยันก่อน ยืนยันแล้วดับหมด · "ไม่ปิด" แล้วไฟไม่เปลี่ยน
  • [ ] ไฟสถานะบนจอ ตัวเลข "ติดอยู่ n จาก 3" และบรรทัดสถานะ ตรงกับหลอดจริงในทุกกรณีที่ทดสอบ รวมทั้งหลัง "ปิดทั้งหมด"
  • [ ] ถ่ายรูปหรือคลิปที่เห็นทั้งจอและหลอดไฟในเฟรมเดียวกัน

เกณฑ์ข้อที่สี่คือข้อที่ตกกันมากที่สุด ทดสอบมันเป็นข้อสุดท้ายเสมอ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

กับดักที่เจอบ่อย

อาการ สาเหตุที่แท้จริง วิธีแก้
จอว่างราว 2 วินาทีหลังส่งโค้ด ไม่ได้เรียก ui.poll() (กฎข้อ 1) เรียก ui.poll() ทุกรอบ
จอกระตุก อัปเดตไม่ครบ ลูปเร็วเกินไป เฟรมถูกทิ้ง (กฎข้อ 5) ใส่ time.sleep_ms(50) ท้ายลูป
CPU ร้อน จอหน่วง ลูปไม่มี sleep เลย (กฎข้อ 2) 50 ms สำหรับ UI เบา · 200 ms แดชบอร์ด
RuntimeError: ui: max 64 widgets สร้าง widget เกินเพดานเฟิร์มแวร์ 64 หรือของเก่าค้าง (กฎข้อ 3 — งบของคอร์สคือ 32) ui.screen() ต้นสคริปต์ แล้วนับ widget ใหม่
ค่าเซนเซอร์ค้างหลังเริ่มใช้ ui auto-task ถูกหยุดตั้งแต่ ui.* ครั้งแรก (กฎข้อ 4) อ่านเซนเซอร์เองในลูป
ตัวหนังสือบน Label ผิดขนาด value= คือขนาดฟอนต์ ไม่ใช่ค่า ใช้เฉพาะ 14/16/20/24/28
กดปุ่มแล้วไม่มีอะไรเกิดขึ้น ลืมเก็บ .id() หรือเทียบ handle ผิด on_ids = [b.id() for b in btn_on] แล้วเทียบ h in on_ids
กล่องยืนยันปิดแล้ว แต่บรรทัดสถานะหายไป status.hide() ตอนกล่องเปิด แล้วลืม show() ในกิ่งที่ปิดกล่อง ทุกกิ่งที่ปิดกล่อง (ยืนยัน และ ไม่ปิด) ต้อง status.show()
ไฟดวงที่สามติดแต่สถานะบอกว่าดับ ไปอ่าน gpio.led(n).value() ซึ่งคืน 0 หลัง hold() (และหลัง brightness() ค่ากลางบนดวงที่ไม่มี PWM) เก็บสถานะไว้ใน led_on[] ของเราเอง
ปุ่มบนจอทำงาน แต่ไฟที่ติดเป็นคนละสีกับปุ่ม หรือมองไม่เห็นดวงไหนติดเลย ใช้เลขดวงที่จำมาแทนที่จะถามบอร์ด — สองบอร์ดเรียงดวงไม่เหมือนกัน และ Dev Kit ดวง 0-1 อยู่บนโมดูล ให้ LED_IDX มาจากชื่อใน gpio.board_info()["led_names"] แบบในเฉลย
กลับจากเมนูอื่นแล้วจอว่าง ออกจากหน้า Playground = widget ถูกทำลาย ส่งโค้ดใหม่ และอยู่หน้า Playground ตลอด

ห้าแถวแรกคือกฎเหล็กห้าข้อในรูปของอาการจริงที่จะเจอ อ่านซ้ำก่อนเริ่มเขียน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ลงมือทำ — เติมช่องว่างในไฟล์ฝึก

เปิด s04_touch_panel.py มีช่องว่างให้เติม 6 จุด

for i in range(len(LED_IDX)):
    # เติม: btn_on.append(ui.Button("เปิด", x=184, y=y, w=144, h=88, color=0x30A46C, value=20))
    pass
    btn_off.append(ui.Button("ปิด", x=360, y=y, w=144, h=88, color=0x3A4150, value=20))

def set_led(i, on):
    led_on[i] = on
    if on:
        # เติม: gpio.led(LED_IDX[i]).on()
        pass
        lamps[i].value(1)
    else:
        # เติม: gpio.led(LED_IDX[i]).off()
        pass
        lamps[i].value(0)

if h in on_ids:
    # เติม: set_led(on_ids.index(h), True)
    pass
elif h in off_ids:
    # เติม: set_led(off_ids.index(h), False)
    pass
เติมแล้วต้องเห็นอะไร ท่า 2 เสร็จ เห็นปุ่มเปิดครบสามแถว กดยังไม่ทำงาน ท่า 4 เสร็จ เรียก set_led(0, True) แล้วไฟติด ท่า 6 เสร็จ แตะจอแล้วทั้งแผงมีชีวิต

หกจุดนี้เรียงตามลำดับที่โปรแกรมทำงานจริง เติมจากบนลงล่างแล้วส่งขึ้นบอร์ดทุกครั้ง

อย่าเติมครบหกจุดแล้วค่อยรันทีเดียว เติมทีละจุดแล้วรัน จะรู้ทันทีว่าจุดไหนพัง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ตัวอย่างของบทเรียน 2.4–2.6 — สามไฟล์แรกทำในบทเรียนให้จบ ที่เหลือเปิดตามอาการ

ต้องทำในบทเรียน · เปิดตามลำดับนี้ ทั้งชุดราว 40 นาที

ลำดับ · เรื่อง · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · วาง widget ตัวแรก · 10 นาที 01_first_widgets.py ลองสร้างปุ่มโดยไม่ใส่ x กับ y ดูสักครั้ง แล้วใส่พิกัดเอง · จะเห็นว่าจอจัดวางให้เองจนข้อความทับกัน และ widget อยู่ค้างจนกว่าจะล้าง
2 · ใครส่ง event อะไร · 15 นาที 02_event_types.py แตะของจริงสี่ตัวแล้วเทียบกับ type ที่ไฟล์บอกไว้ล่วงหน้า · จะไม่เสียเวลาทั้งบทเรียนกับ ui.Checkbox ที่ส่ง toggled ไม่ใช่ clicked
3 · จอกับหลอดต้องพูดตรงกัน · 15 นาที 03_switch_matches_led.py กด ALL OFF แล้วดูว่าสวิตช์บนจอเด้งกลับเองไหม · จะได้แบบแผน "ฟังก์ชันเดียวที่มีสิทธิ์เปลี่ยนสถานะ" ซึ่งคือเกณฑ์ MVP ข้อที่ตกกันมากที่สุด

ติดตรงไหน เปิดอันนี้

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
ร่างผังจอไว้แล้ว แต่ไม่รู้ว่าวางได้อีกกี่ตัวก่อนจะชนงบ 06_layout_budget.py — ดูแถบโควตาที่ใช้ไปกี่ในงบคอร์ส 32 (เพดานเฟิร์มแวร์ 64) แล้วลองสร้างตัวที่ 33 ให้เห็นว่าเส้นไหนคือของคอร์ส เส้นไหนคือของบอร์ด แล้วกลับไปนับผังของทีมบนกระดาษก่อนพิมพ์โค้ดบรรทัดแรก
อยากได้ทศนิยมแต่จอขึ้นจำนวนเต็ม 04_seg7_takes_text.py — .value(50) ได้ 50 ทศนิยมต้องส่งเป็นข้อความ
แตะปุ่มแล้วไม่แน่ใจว่าบอร์ดรับไปหรือยัง 05_sound_feedback.py — เสียงเป็นแบบยิงแล้วลืม จอจึงต้องเป็นพยานแทนหูเสมอ และ ui.tone() รับโน้ต MIDI ไม่ใช่เฮิรตซ์
สร้าง widget ไปแล้วแต่อยากย้าย ย่อ ซ่อน หรือลบทิ้งเพื่อคืนโควตา 07_find_move_hide_delete.py — ui.list() บอกว่ามีอะไรอยู่ ui.get(id) เอากลับคืนมาจากเลข แล้ว .pos() .size() .show() .hide() .delete() ทำงานต่อได้ทันที · .hide() ไม่คืนโควตา มีแต่ .delete() ที่คืน
ต้องใช้ช่องเลือกหรือช่องพิมพ์ แต่ไม่รู้ว่ามันส่ง event แบบไหน 08_dropdown_textarea.py — Checkbox Dropdown Textarea ครบทั้งสามตัวที่ชุดบทเรียนนี้ไม่ได้ใช้ · Dropdown ส่งกลับมาแค่ ลำดับ ของตัวเลือก ไม่ได้ส่งข้อความ

อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของบทเรียน 2.4–2.6 แต่เป็นแบบแผนที่ทุกแผงควบคุมในโรงงานใช้: 07_hold_to_confirm.py คำสั่งที่ย้อนกลับไม่ได้ ต้องกดค้างเพื่อยืนยัน พร้อมตัวบอกความคืบหน้าที่ปล่อยมือแล้วยกเลิกได้ และห้ามนับถอยหลังด้วย sleep เพราะจะทำให้แตะอย่างอื่นไม่ได้ทั้งช่วง

สามไฟล์แรกคือของที่ต้องเปิดจริงในบทเรียน ตารางล่างเปิดเฉพาะตอนเจออาการนั้น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เชื่อมโยงรากฐาน — วันนี้เราแตะอะไรไปบ้าง

สมองกลฝังตัว event-driven สองคอร์ ทรัพยากรมีเพดานตายตัว Python และ CS list · dict · comprehension ลูปที่คืนเวลาให้ระบบ ออกแบบระบบ single source of truth แยกสถานะออกจากการแสดงผล สามเสาที่วันนี้แตะพร้อมกัน เสาที่สามคือเสาที่จะยังใช้ได้แม้เปลี่ยนภาษาและเปลี่ยนบอร์ด

ฝั่งระบบสมองกลฝังตัว
สถาปัตยกรรม event-driven บนระบบสองคอร์ · คิวเหตุการณ์และการ poll ข้ามคอร์ · ข้อจำกัดของทรัพยากรที่มีเพดานตายตัว (64 widgets) · ขา GPIO ที่เขียนได้แต่อ่านไม่ได้

ฝั่ง Python และวิทยาการคอมพิวเตอร์
list ของอ็อบเจกต์และ list comprehension · dict กับการเข้าถึงด้วยคีย์ · การนิยามฟังก์ชันเพื่อรวมงานที่ต้องทำพร้อมกัน · .index() เพื่อ map จาก handle กลับเป็นลำดับ · ลูปที่ไม่มีวันจบกับการคืนเวลาให้ระบบ

ฝั่งการออกแบบระบบ
single source of truth · การแยก "สถานะ" ออกจาก "การแสดงผล" · การออกแบบให้ทุกเส้นทางการเปลี่ยนแปลงผ่านประตูเดียว · การทดสอบด้วยการมองสองฝั่งพร้อมกัน

แนวคิด single source of truth จะกลับมาอีกในบทเรียน 4.4–4.6 ตอนที่คำสั่งมาจาก MQTT แทนที่จะมาจากนิ้ว

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

งานทำเอง 30% + สรุปบทเรียน

สร้าง widget วางตำแหน่งเอง handle .id() เก็บไว้เทียบ event loop poll · dispatch · act จอตรงกับของจริง ประตูเดียว ชุดบทเรียนถัดไป pot + CapSense สี่อย่างที่ติดมือไปชุดบทเรียนถัดไป ชุดบทเรียนถัดไปเปลี่ยนแค่ต้นทางของข้อมูล โครงลูปยังเป็นตัวเดิม

วันนี้เราได้:
สร้าง widget เป็น วางตำแหน่งเป็น และรู้ว่า value= คือขนาดฟอนต์ · เข้าใจ handle กับ .id() · เขียน event loop ครบวงจร poll → dispatch → act → sleep · ต่อเหตุการณ์บนจอเข้ากับ gpio.led() · และที่สำคัญที่สุด รักษาให้จอกับของจริงตรงกันเสมอ

การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ "ต่อยอด" จดลงบันทึกการเรียน

ชุดบทเรียนถัดไป: เราจะเพิ่ม input แบบอนาล็อกเข้ามา — หมุนลูกบิด pot และเลื่อนนิ้วบนแถบ CapSense แล้วเอาค่ามาขับ ui.Bar บนไม้บรรทัด ui.Scale พร้อมรู้จักการกรองสัญญาณรบกวนด้วย dsp.EMA · ส่วนขยาย 17 ของวันนี้ทำให้เห็นแล้วว่าค่าหนึ่งค่าบนจอออกไปถึงหน้าเว็บได้อย่างไร ค่าลูกบิดกับแผ่นสัมผัสของชุดบทเรียนหน้าก็เป็นค่าแบบเดียวกัน

event loop ที่เขียนวันนี้ จะเป็นโครงเดิมของทุกโปรแกรมที่เหลือในคอร์สนี้

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เฉลย s04_touch_panel.py — ส่วนที่หนึ่ง: เตรียมของ

อ่านให้เข้าใจ แล้วพิมพ์เอง อย่าคัดลอกวาง

import ui
import gpio
import time

BTN_TEXT = ["แดง", "เขียว", "น้ำเงิน"]              # สามสีของแผง เรียงตรงกับ LED_IDX ข้างล่าง
COL_ON = [0xE53935, 0x43A047, 0x1E88E5]           # สีไฟสถานะตอนติด เรียงตรงกับ BTN_TEXT
COL_OFF = 0x171B22                                 # สีเทาของปุ่มที่ไม่ได้กำลังทำงาน
COL_TEXT, COL_DIM, COL_CARD = 0xE8EAED, 0x9AA3AF, 0x171B22

# ดวงจริงของแต่ละสี ถามจากชื่อที่บอร์ดรายงาน
# Dev Kit รายงาน RGB_RED/GREEN/BLUE ครบ -> ใช้ชื่อ
# Eva Kit ไม่ครบ -> ดวง 0 1 2 คือ แดง เขียว น้ำเงิน
LED_NAMES = gpio.board_info()["led_names"]
RGB_NAMES = ("RGB_RED", "RGB_GREEN", "RGB_BLUE")
if all(n in LED_NAMES for n in RGB_NAMES):
    LED_IDX = [LED_NAMES.index(n) for n in RGB_NAMES]
else:
    LED_IDX = [0, 1, 2]

led_on = [False] * len(LED_IDX)   # ความจริงอยู่ที่นี่

ui.screen()
time.sleep_ms(200)
สี่ list ที่เดินด้วยดัชนีเดียว "แดง" "เขียว" "น้ำเงิน" BTN_TEXT COL_ON Eva 0 · Dev 2 Eva 1 · Dev 4 Eva 2 · Dev 3 LED_IDX · ถามจากบอร์ด False False False led_on · i เดียวใช้ได้ทั้งสามแถว

led_on ถูกประกาศ ก่อน สร้าง widget ใด ๆ เพราะมันคือแกนกลางของโปรแกรม ส่วนจอเป็นแค่ผู้รายงาน · สี่สีบรรทัดที่สี่คือจานสีของหลักสูตร — COL_OFF เท่ากับ COL_CARD โดยตั้งใจ ปุ่มที่ไม่ได้กำลังทำงานต้องกลืนไปกับพื้น

การเก็บสี ON ของแต่ละสีเป็น list คู่ขนานกับ BTN_TEXT ทำให้ทุกอย่างอ้างด้วยดัชนี i ตัวเดียวได้ตลอดทั้งไฟล์ — แถวที่ 0 คือแดงทั้งข้อความ ทั้งสี ทั้ง gpio.led(LED_IDX[0]) · LED_IDX คือ list ที่สี่ที่เดินด้วยดัชนีเดียวกัน แต่ค่าข้างในถามจากบอร์ด เพราะ Eva Kit กับ Dev Kit เรียงดวงไม่เหมือนกัน (บน Dev Kit สีน้ำเงินคือดวง 3 ไม่ใช่ 2 และดวง 0-1 อยู่บนโมดูล)

ข้อมูลที่คู่กันให้เรียงให้ตรงกัน แล้วดัชนีเดียวจะพาเราไปได้ทั้งโปรแกรม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เฉลย — ส่วนที่สอง (1/2): สามแถวของแผง

ui.Label("แผงควบคุม LED ของทีม", x=24, y=8, color=COL_TEXT, value=28)
status = ui.Label("พร้อมรับคำสั่ง", x=360, y=12, color=COL_DIM, value=20)
...
ROW_TOP = 52           # ขอบบนของแถวแรก
ROW_PITCH = 120        # ปุ่มสูง 88 + ระยะระหว่างเป้าสัมผัส 32 = 120
lamps = []
btn_on = []
btn_off = []
for i in range(len(LED_IDX)):
    y = ROW_TOP + i * ROW_PITCH
    lamps.append(ui.Led(x=24, y=y + 20, w=48, h=48, color=COL_ON[i], value=0))
    ui.Label(BTN_TEXT[i], x=88, y=y + 32, color=COL_DIM, value=16)
    btn_on.append(ui.Button("เปิด", x=184, y=y, w=144, h=88, color=0x30A46C, value=20))
    btn_off.append(ui.Button("ปิด", x=360, y=y, w=144, h=88, color=0x3A4150, value=20))
...
on_ids = [b.id() for b in btn_on]
off_ids = [b.id() for b in btn_off]
หน้าตาจริงหลังท่านี้ แผงควบคุม LED ของทีม พร้อมรับคำสั่ง แดง เขียว น้ำเงิน เปิด ปิด เปิด ปิด เปิด ปิด การ์ดขวา (2/2) หัวเรื่อง 2 + ไฟสถานะ 3 + ป้าย 3 + ปุ่มสีละคู่ 6 = 14 widget หลังท่านี้

ทำไมเลิกใช้ ui.Switch ตัวเดียวคุมสามสี — สวิตช์คือปุ่มสลับ และปุ่มสลับบอกไม่ได้ว่าตอนนี้อยู่สถานะไหน คนกดต้องอ่านจากที่อื่นแล้วเดา แผงควบคุมจริงจึงแยก ปุ่มเปิด กับ ปุ่มปิด เสมอ ปุ่ม "เปิด" ที่กดซ้ำสิบครั้งได้ผลเดียวกับกดครั้งเดียว ซึ่งสำคัญมากตอนคนกดซ้ำเพราะไม่แน่ใจ

ไฟสถานะแยกจากปุ่ม ไม่ใช่ให้ปุ่มเปลี่ยนสีเอง เพราะสองอย่างนี้คนละหน้าที่ — ไฟตอบว่า "ตอนนี้เป็นยังไง" ปุ่มตอบว่า "สั่งอะไรได้" และไฟผ่านการทดสอบขาวดำ ส่วนปุ่มที่เปลี่ยนสีไม่ผ่าน · ปุ่มสูง 88 และห่างกัน 32 ตามระยะนิ้วจริง ปุ่มเปิดใช้สีสถานะ ok 0x30A46C ปุ่มปิดใช้สีปุ่มรอง 0x3A4150

ทุกอย่างในแถวอ้างด้วย i ตัวเดียว — ข้อความ สี ไฟ ปุ่ม และดวงจริง LED_IDX[i]

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เฉลย — ส่วนที่สอง (2/2): การ์ดคำสั่งทั้งชุด และกล่องยืนยันที่สร้างแล้วซ่อน

ui.Panel(x=520, y=48, w=248, h=308, color=COL_CARD, min=COL_DIM, max=12, value=1)
lbl_count = ui.Label("ติดอยู่ 0 จาก " + str(len(LED_IDX)) + " ดวง", x=536, y=60,
                     color=COL_TEXT, value=20)
ui.Label("คำสั่งทั้งชุด", x=536, y=96, color=COL_DIM, value=16)
btn_all_on = ui.Button("เปิดทั้งหมด", x=536, y=132, w=216, h=88, color=0x30A46C, value=20)
btn_all_off = ui.Button("ปิดทั้งหมด", x=536, y=252, w=216, h=88, color=0x3A4150, value=20)
...
box = ui.MsgBox("ปิดทั้งหมด\nไฟทุกสีจะดับพร้อมกัน", x=48, y=88, w=496, h=160,
                color=COL_CARD)
btn_yes = ui.Button("ปิดทั้งหมด", x=568, y=88, w=152, h=88, color=0x3A4150, value=20)
btn_no = ui.Button("ไม่ปิด", x=568, y=208, w=152, h=88, color=0x3A4150, value=20)
box.hide()
btn_yes.hide()
btn_no.hide()

นับ widget กันชัด ๆ ทั้งหน้า: หัวเรื่อง 1 + บรรทัดสถานะ 1 + ไฟสถานะ 3 + ป้ายชื่อสี 3 + ปุ่มเปิด 3 + ปุ่มปิด 3 + การ์ดขวา 1 + ป้ายบนการ์ด 2 + คำสั่งชุด 2 + กล่องยืนยันกับปุ่มคำตอบ 3 = 22 ตัว — ต่ำกว่างบของคอร์ส 32 (เพดานเฟิร์มแวร์ 64) เหลือที่ให้การบ้านต่อยอด

ปุ่มล่างของการ์ดจบที่ y=340 พอดี ต่ำกว่านั้นคือมุมที่ปุ่ม Console จองไว้ · กล่องยืนยันกับปุ่มคำตอบสองปุ่ม สร้างพร้อมหน้าจอแล้วซ่อนไว้ ไม่ใช่สร้างตอนกด — การสร้างของตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · คำในกล่องบอก สิ่งที่จะเกิด ไม่ใช่ถามลอย ๆ ว่า "แน่ใจไหม" · ปุ่มคำตอบเป็น ui.Button จริงสองปุ่ม เพราะ MsgBox ของเฟิร์มแวร์นี้มีแค่หัวเรื่องกับเนื้อความ (บรรทัดแรกของ text คือหัวเรื่อง) ไม่มีปุ่มคำตอบในตัว (ui_widget_mgr.c:1849-1880)

ทุกครั้งที่ออกแบบหน้าใหม่ ให้นับ widget บนกระดาษก่อนพิมพ์ ไม่ใช่ไปเจอ RuntimeError ตอนรัน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เฉลย — ส่วนที่สาม: หัวใจของความตรงกัน

def show_status():
    n = 0
    for i in range(len(LED_IDX)):
        if led_on[i]:
            n += 1
    lbl_count.text("ติดอยู่ " + str(n) + " จาก " + str(len(LED_IDX)) + " ดวง")

def set_led(i, on):
    # i คือแถวของแผง (0 แดง 1 เขียว 2 น้ำเงิน) ดวงจริงอยู่ที่ LED_IDX[i]
    led_on[i] = on              # 1) จำไว้ก่อน
    if on:
        gpio.led(LED_IDX[i]).on()   # 2) สั่งของจริง
        lamps[i].value(1)       # 3) ไฟบนจอสะท้อนของจริง
    else:
        gpio.led(LED_IDX[i]).off()
        lamps[i].value(0)       # หรี่ ไม่ใช่หาย
    show_status()               # 4) รายงานให้ครบ

for i in range(len(LED_IDX)):
    set_led(i, False)           # เริ่มจากที่รู้แน่
ทำไมต้องสั่งดับตอนเริ่ม สภาพที่เจอตอนโปรแกรมเริ่ม ทีมก่อนหน้าทิ้งไฟติดค้างไว้ set_led(i, False) ทุกแถว ดับจริง และจอรายงานว่าดับ

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

โปรแกรมที่ดีไม่เชื่อสภาพเริ่มต้นที่ตัวเองไม่ได้เป็นคนกำหนด

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เฉลย — ส่วนที่สี่: ลูปเหตุการณ์

while True:
    for ev in ui.poll():
        h = ev['handle']
        t = ev['type']
        if t != 'clicked':
            continue
        if h in on_ids:
            set_led(on_ids.index(h), True)
        elif h in off_ids:
            set_led(off_ids.index(h), False)
        elif h == btn_all_on.id():
            for i in range(len(LED_IDX)):
                set_led(i, True)
        elif h == btn_all_off.id() and not asking:
            asking = True
            box.show()
            btn_yes.show()
            btn_no.show()
        elif h == btn_yes.id() and asking:
            asking = False
            for i in range(len(LED_IDX)):
                set_led(i, False)
            box.hide()
            btn_yes.hide()
            btn_no.hide()
    time.sleep_ms(50)
ทำไมไม่ใช้ปุ่มเดียวสลับ ปุ่มเดียวเขียนว่า "สลับ" คนกดต้องรู้ก่อนว่าตอนนี้เปิดหรือปิด เดาผิดเมื่อไร ก็สั่งตรงข้ามกับที่ตั้งใจ สองปุ่มแยกกัน เปิด กับ ปิด กดซ้ำสิบครั้ง ได้ผลเท่ากดครั้งเดียว ไม่ต้องรู้สถานะก่อนกด ทุกปุ่มส่ง 'clicked' เหมือนกันหมด จึงต้องเทียบ handle ควบไปด้วยเสมอ

set_led(on_ids.index(h), True) สั่งเปิด ไม่ใช่สั่งสลับ — คำสั่งที่ระบุปลายทางแบบนี้เรียกว่า idempotent กดซ้ำกี่ครั้งก็ได้ผลเดิม ต่างจากคำสั่งสลับที่ผลขึ้นกับสถานะก่อนหน้า ซึ่งเป็นสิ่งที่คนกดมองไม่เห็น · ทุกปุ่มส่ง 'clicked' เหมือนกันหมด จึงกรอง type ทิ้งรอบเดียวข้างบน แล้วแยกด้วย handle อย่างเดียว — สั้นกว่าและพลาดยากกว่าการเช็กควบสองอย่างทุกกิ่ง

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

while True ที่ไม่มี time.sleep_ms() คือบั๊กที่ไม่แสดงตัวเป็น error แต่ทำให้ทั้งระบบช้าลง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เฉลย · ทำไมเรียงหกท่าแบบนี้ ไม่ใช่สุ่มเรียง

1 ล้างจอ 2-3 แผงกับปุ่ม 4 set_led() 5 ตั้งต้นให้ดับ 6 event loop สิ่งที่มองเห็น สิ่งที่ควบคุมได้ สิ่งที่ตอบสนอง ลำดับดีบักมาตรฐานของงาน UI ทุกแพลตฟอร์ม — ล้มตรงไหนก็รู้ทันทีว่าตรงไหน

ท่า 1 ล้างจอ + หัวเรื่อง พิสูจน์ว่าช่องทางถึงจอใช้ได้ ถ้าหัวเรื่องยังไม่ขึ้น อย่าเพิ่งเขียนอะไรต่อ

ท่า 2 แถวของแต่ละดวง มาก่อนตรรกะ เพราะ "มองเห็น" ง่ายที่สุด ตอนนี้กดยังไม่มีอะไรเกิด — และนั่นถูกต้องแล้ว

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

ท่า 4 set_led() มาก่อนลูป เพื่อให้ทดสอบตรง ๆ ได้ด้วย set_led(0, True) บรรทัดเดียว ไฟติดแปลว่าฝั่งฮาร์ดแวร์เรียบร้อย

ท่า 5 ตั้งต้นให้ดับ มาก่อนลูป เพราะ set_led() ต้องเรียกได้ตั้งแต่บรรทัดแรกที่ทำงาน และทุกการรันต้องเริ่มจากจุดเดียวกัน

ท่า 6 event loop มาสุดท้าย เพราะมันเป็นแค่ "คนเดินสาร" ถ้าห้าท่าบนถูกหมด ท่านี้จะสั้นและตรงไปตรงมา

ถ้าเขียน event loop ตั้งแต่แรกแล้วมันเงียบ เราจะไม่รู้เลยว่าพังที่จอ ที่ไฟ หรือที่ตรรกะ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เชื่อมจุดให้เห็นภาพ — วันนี้อยู่ตรงไหนของเส้นทาง

บทเรียน 2.1–2.3 · ที่ผ่านมา สั่งไฟด้วยโค้ดล้วน ยังไม่มีคนมาสั่ง บทเรียน 2.4–2.6 · วันนี้ คนแตะจอ → ไฟจริงเปลี่ยน จอกับของจริงตรงกัน บทเรียน 2.7–3.9 · ถัดไป เซนเซอร์ขับหน้าจอ Arc · Chart · Dashboard บทเรียน 4.1–5.3 คำสั่งมาจากเน็ต แทนที่จะมาจากนิ้ว event loop วันนี้ คือโครงเดียวกับที่รับคำสั่ง MQTT ในบทเรียน 4.4–4.6

คำถามคิดต่อ: ถ้าคำสั่งเปิดไฟมาจากอินเทอร์เน็ตแทนนิ้ว โค้ดส่วนไหนต้องเปลี่ยน · ส่วนไหนไม่ต้องเปลี่ยนเลย · แล้วเราจะรู้ได้อย่างไรว่าคำสั่งจากเน็ตมาถึงจริง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ใช้จริงที่ไหน — สี่มุมของแผงควบคุมสัมผัสในสนามจริง

โรงงาน · HMI หน้าเครื่องจักร จอสัมผัสสั่งเดินเครื่อง หยุด และรีเซ็ตความผิดพลาด จอต้องสะท้อนสถานะรีเลย์จริง ไม่ใช่คำสั่งล่าสุด แพทเทิร์นเดียวกับ set_led() ของเราวันนี้ อาคาร · แผงคุมไฟและแอร์ แผงที่ผนังห้องประชุม เปิด-ปิดเป็นโซน "ปิดทั้งหมด" คือปุ่มที่คนใช้บ่อยที่สุดตอนออกจากห้อง ไฟสถานะต้องกลับมาตรงเมื่อโซนถูกปิดจากทางอื่น การแพทย์ · แผงคุมเตียงและปั๊ม ปุ่มบนจอที่สั่งของจริงซึ่งพลาดไม่ได้ ต้องยืนยันจากอุปกรณ์ก่อนจึงเปลี่ยนสถานะบนจอ อย่าให้จอรายงานสิ่งที่ยังไม่เกิดขึ้นจริง เกษตร · ตู้คุมปั๊มน้ำและวาล์ว แผงหน้าตู้ควบคุมโซนรดน้ำทีละแปลง คำสั่งเข้ามาได้สองทาง: หน้าตู้ และจากมือถือ สองทางเข้าหนึ่งความจริง — เหมือน set_led() วันนี้
จอเครื่องเฝ้าสัญญาณชีพผู้ป่วยที่แสดงตัวเลขและกราฟหลายช่องเครื่องวัดรังสีไกเกอร์ที่บอกระดับด้วยเสียงคลิกเครื่องวัดอัตราไต่ระดับของร่มร่อน ที่บอกค่าเป็นเสียง
ซ้าย: หน้าจอมอนิเตอร์ผู้ป่วยจริงขณะมีสัญญาณเตือน TACHY แถบสีแดงพาดบนสุด — เมื่อของสำคัญ ระบบจะยกมันขึ้นเหนือทุกอย่างบนจอ — ภาพ: US Navy / Wikimedia Commons — สาธารณสมบัติ  |  กลาง: เครื่องนับรังสีที่รายงานด้วยเสียงคลิก ส่วนติดต่อผู้ใช้ที่ไม่มีจอเลย — ภาพ: TimVickers / Wikimedia Commons — สาธารณสมบัติ  |  ขวา: เครื่องวัดอัตราไต่ของนักร่มร่อน มือทั้งสองข้างไม่ว่าง จอจึงไม่ใช่ทางออก ระบบนี้พูดออกมาเป็นเสียงแทน — ภาพ: Flyout / Wikimedia Commons — CC BY-SA 3.0

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

ทั้งสี่มุมเจอโจทย์เดียวกันหมด: มีคนสั่งได้หลายทาง แต่ความจริงต้องมีชุดเดียว

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ดูเพิ่มเติมนอกเวลา + อ่านต่อ

What is Switch Bounce and How to Debounce · DigiKey
ของนอกเวลา ต้องมีอินเทอร์เน็ต

ทำไมคลิปนี้เกี่ยวกับการบ้านข้อ 4

ข้อ 4 ให้เอาปุ่มผู้ใช้จริงบนบอร์ด (gpio.button(0) — เรียกด้วยชื่อจาก .name() ไม่ใช่ป้ายบนแผ่นวงจร) มาทำงานร่วมกับปุ่มบนจอในลูปเดียวกัน คลิปนี้ทบทวนว่าทำไมปุ่มกลไกกดครั้งเดียวถึงกลายเป็นหลายเหตุการณ์

ปุ่มบนจอไม่มีปัญหานี้เพราะ CM55 จัดการให้แล้ว — ปุ่มจริงต้องทำเอง

อ่านต่อสำหรับคนอยากรู้ลึก

  • time.ticks_ms() / ticks_diff() ที่ใช้จับจังหวะลูป — MicroPython docs: https://docs.micropython.org/en/latest/library/time.html
  • จอ MIPI-DSI และ backlight ของ Eva Kit — KIT_PSE84_EVAL user guide §3.2.2.8 (หน้า 75–81)

ปุ่มบนจอไม่ต้อง debounce เพราะมีคนทำให้แล้ว ปุ่มจริงต้องทำเอง — จำความต่างนี้ไว้ตอนทำข้อ 4

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ส่วนขยาย (ไม่บังคับ) — แผงของเราบน broker

หน้าจอของ 17_panel_to_broker.py: แตะบนจอบอร์ด แล้วหน้าเว็บของเพื่อนเห็น สั่งจากเว็บ แล้วแถบบนจอขยับ

ภาพจากตัวจำลอง bento\_sim — โค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด · แสดงหน้าจอที่ 17_panel_to_broker.py สร้างหลังแก้ `TEAM` เป็นเลขทีมแล้ว · สายและตัวนับบนภาพเป็นค่าแทนบนเครื่องโฮสต์ ไฟล์นี้ยังไม่เคยรันบนบอร์ด

17_panel_to_broker.py เอาปุ่มกับแถบเลื่อนของวันนี้ไปต่อกับ mqtt ของบทเรียน 1.4–1.6 ชื่อ broker ทีม และหัวข้อเหมือนบทเรียน 1.4–1.6 และ 2.1–2.3 ทุกตัวอักษร · ค่าตั้งต้น TEAM = "teamXX" จงใจให้ไม่ยอมรัน แก้เป็นเลขทีมของเราก่อน

ของบนจอ ไปที่ไหน เพราะ
แตะปุ่ม "ส่ง event" event ทันที เรื่องที่เกิดครั้งหนึ่ง
ลากแถบเลื่อน telemetry ทุก 2 วินาที ค่าที่เป็นอยู่ แถบยิง value_changed ถี่มากระหว่างลาก
{"cmd":"set","v":80} จากเว็บ แถบกับตัวเลขบนจอขยับ ผ่านฟังก์ชันเดียว set_value() แบบ set_led() ของไฟล์ 03

คำสั่ง set เป็นคำสั่งใหม่ของไฟล์นี้ พิมพ์ในช่อง "พิมพ์ JSON เอง" ของหน้าเว็บ · say กับ beep ใช้ปุ่มที่มีอยู่แล้ว · ข้อความ say ถูกกรองให้เหลือแค่ ASCII กับไทยก่อนขึ้นจอ และ v ที่แปลงเป็นเลขไม่ได้ (เช่น 1e999) ถูกทิ้ง ไม่ทำให้โปรแกรมล้ม เพราะใครก็ส่งเข้าหัวข้อนี้ได้ · เฟิร์มแวร์นี้ส่ง retain ไม่ได้ ไฟล์จึงส่ง telemetry ซ้ำเอง หน้าเว็บที่เปิดทีหลังเห็นภายในสองวินาที

ไม่อยู่ในเกณฑ์ผ่านของชุดบทเรียนนี้ · ต้องมี WiFi ที่ออกพอร์ต 1883 ได้ ซึ่งเครือข่ายขององค์กรยังไม่ได้ทดสอบ · Emulator ใน ide.tesaiot.dev ส่งถึง broker จริงได้แล้วผ่าน WebSocket

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ต่อยอด — คิดต่อเอง (เลือกทำ 1 ข้อ)

1 · ไฟวิ่ง START / STOP ใช้ flag ในลูปเดียว 2 · ตัวนับ นับครั้งที่กด + ปุ่ม RESET 3 · ล็อกแผง Switch LOCK บอกให้รู้ว่าปิดอยู่ 4 · ปุ่มจริง ปุ่มบนบอร์ดร่วมกับจอ ระวัง debounce ทั้งสี่ข้อใช้ event loop เดิม ไม่ต้องเขียนลูปใหม่

ข้อ 1 · โหมดไฟวิ่งสั่งจากจอ — เพิ่มปุ่ม START/STOP ที่เปิด-ปิดโหมดไฟวิ่ง (chaser) ของบทเรียน 2.1–2.3 ใช้ตัวแปร flag ในลูปเดียวกับ ui.poll() ห้ามใช้ลูปซ้อนที่ทำให้แตะปุ่มอื่นไม่ได้ระหว่างไฟวิ่ง

ข้อ 2 · ตัวนับการกดและปุ่มรีเซ็ต — เก็บจำนวนครั้งที่แต่ละสีถูกสั่งเปิด แสดงบน Label เพิ่มอีกบรรทัด และมีปุ่ม RESET ที่ล้างตัวนับกลับเป็นศูนย์ทั้งสามสี

ข้อ 3 · ล็อกแผงควบคุม — เพิ่ม ui.Switch ชื่อ LOCK เมื่อล็อกอยู่ ปุ่มทั้งหมดต้องกดไม่ได้ และบรรทัดสถานะต้องบอกให้รู้ — ทำอย่างไรให้ผู้ใช้เข้าใจว่าปุ่มถูกปิดการใช้งาน ไม่ใช่ค้าง

ข้อ 4 · ปุ่มจริงกับปุ่มบนจอทำงานร่วมกัน — อ่าน gpio.button(0).is_pressed() ในลูปเดียวกัน ให้ปุ่มผู้ใช้บนบอร์ด (ชื่อจาก btn.name() — บน Dev Kit ห้ามโยกสวิตช์บนฐาน หลายตัวคือสวิตช์ตัดไฟ) สลับไฟสีแดงได้ด้วย โดยสถานะบนจอยังตรงเสมอ (ระวัง debounce แบบที่ทำในบทเรียน 2.1–2.3)

ทั้งสี่ข้อเดินบน event loop เดิม ไม่ต้องเขียนลูปใหม่ — กติกาเดียวที่ใช้ได้กับทุกข้อคือ ห้ามให้จังหวะของงานหนึ่งไปหยุดการแตะปุ่มของอีกงานหนึ่ง ทุกอย่างที่ต้องรอ ให้จำเวลาไว้ในตัวแปรแล้วกลับมาดูรอบหน้า ไม่ใช่หลับรอด้วย sleep · ทีมที่ทำข้อ 2 ให้กลับไปดูแบบแผน "ฟังก์ชันเดียวที่มีสิทธิ์เปลี่ยนสถานะ" ใน 03_switch_matches_led.py เพราะตัวนับกับปุ่ม RESET คือสถานะชุดเดียวกันที่ถูกแตะจากสองทาง · ทีมที่อยากเพิ่มเสียงตอบรับตอนแตะ เปิด 05_sound_feedback.py

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

สาม widget ที่แยกหน้าจอ HMI ออกจากหน้าจอเล่น ๆ

หน้าจอของ 09_scale_led_spinbox.py: สาม widget ที่แยกหน้าจอ HMI ออกจากหน้าจอเล่น ๆ

ภาพจากตัวจำลอง bento_sim — โค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด · แสดงหน้าจอที่ 09_scale_led_spinbox.py สร้าง
widget ตอบคำถามอะไร ทำไมหน้าจอควบคุมจริงต้องมี
ui.Scale ค่านี้สูงไหมเมื่อเทียบกับพิสัย เลข 72 ลอย ๆ ไม่บอกอะไร · 72 บนไม้บรรทัด 0–100 บอกทันที
ui.Led ตอนนี้สถานะอะไร ไฟแผงควบคุมที่คนมองปราดเดียวรู้ ไม่ต้องอ่านตัวหนังสือ
ui.Spinbox ผู้ใช้ป้อนค่าที่ต้องการยังไง แถบเลื่อนป้อน 23.75 ไม่ได้ ช่องนี้ได้

ui.Scale ไม่รับ .value() — มันคือไม้บรรทัด ไม่ใช่หน้าปัด ตัวที่ขยับคือสิ่งที่เราวางทับลงไปเอง ในไฟล์นี้คือ ui.Bar ที่วางเหนือมัน · นี่คือเรื่องที่คนเข้าใจผิดบ่อยที่สุดใน LVGL และโค้ด C ของเฟิร์มแวร์เราเองก็ทำแบบนี้ (ยกเว้นแบบวงกลม ที่มีเข็มจริงผ่าน PROP_SCALE_NEEDLE — fw 2026-08-20 ขึ้นไป บทเรียน 3.1–3.3 สอน)

ui.Led สั่ง .value(0) แล้วหรี่ ไม่ใช่หาย — ตั้งใจให้เป็นแบบนั้น ไฟแผงควบคุมที่หายไปตอนดับ แย่กว่าไฟที่หรี่ลง เพราะคนดูแยกไม่ออกว่าดับหรือจอเสีย

กติกาที่แผงจริงใช้ — ให้ติดทีละดวง ถ้าติดพร้อมกันสองดวงตอนคาบเกี่ยว คนอ่านไม่ออกว่าตกลงสถานะอะไร

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ตารางกับรายการ แทนป้ายที่จัดคอลัมน์เอง

หน้าจอของ 12_table_and_list.py: หลายค่าพร้อมกัน ต้องเป็นตาราง ไม่ใช่ป้ายเรียงกัน

ภาพจาก Emulator ของหลักสูตร (BENTO_IDE/bento-emulator) ซึ่งรันโค้ด MicroPython ชุดเดียวกับที่ลงบอร์ด บนพื้นที่วาด 792x398 เท่ากับจอของทั้งสองบอร์ด · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ 12_table_and_list.py · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด
widget ตอบคำถามอะไร
ui.Table หลายค่าพร้อมกัน โดยคอลัมน์ไม่ขยับตามความยาวของค่า
ui.List รายการที่แตะเลือกได้ มีไอคอนนำสายตา และเลื่อนเองเมื่อยาวเกินกรอบ
ui.Line รูปร่างของค่าที่ผ่านมา วาดจากจุดที่เราป้อนเอง
ui.Picture ไอคอนหนึ่งใบ กำหนดสีได้จากโค้ด

.add_row() เขียนลงแถวถัดจากแถวที่ตัวมันเองเขียนล่าสุด ไม่ใช่แถวที่ยังว่าง — จะแก้ค่าในแถวเดิมต้องใช้ .cell(แถว, คอลัมน์, ข้อความ) ถ้าเผลอใช้ .add_row() ในลูป ตารางจะยาวลงไปเรื่อย ๆ จนเลยกรอบ

ป้ายหลายบรรทัดที่จัดเป็นตารางด้วยการนับพิกเซลเอง จะเลื่อนทันทีที่ค่าเปลี่ยนความยาว — "27.4" กับ "8.0" กว้างไม่เท่ากัน ตารางจริงจัดคอลัมน์ให้

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

สั่งของจริง ต้องถามก่อนหนึ่งครั้ง

หน้าจอของ 13_confirm_before_acting.py: คำสั่งที่ทำให้ของจริงขยับ ต้องถามก่อนหนึ่งครั้ง

ภาพจาก Emulator ของหลักสูตร (BENTO_IDE/bento-emulator) ซึ่งรันโค้ด MicroPython ชุดเดียวกับที่ลงบอร์ด บนพื้นที่วาด 792x398 เท่ากับจอของทั้งสองบอร์ด · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ 13_confirm_before_acting.py · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด
widget ตอบคำถามอะไร
ui.ButtonMatrix ปุ่มหลายใบเป็นตาราง ด้วย widget เดียวและโควตาเดียว
ui.MsgBox กล่องยืนยันที่มีหัวเรื่อง เนื้อความ และปุ่มเป็นของตัวเอง

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

คำยืนยันต้องบอกสิ่งที่จะเกิดขึ้น ไม่ใช่คำว่า "ยืนยันไหม" ซึ่งไม่ได้เพิ่มข้อมูลให้คนตัดสินใจเลยสักอย่าง

ทั้ง 12_table_and_list.py และ 13_confirm_before_acting.py ถูกรันจริงในชุดทดสอบของ Emulator ทุกครั้งที่ไลบรารีเปลี่ยน — ถ้าวันไหน widget ตัวใดวาดไม่ออก ชุดทดสอบล้มก่อนที่ห้องเรียนจะเจอ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

หน้าจอเดียวไม่พอเมื่อไร และจอที่สองราคาเท่าไร

หน้าจอของ 10_tabview_second_screen.py: จอที่สองที่ไม่ต้องเขียนโปรแกรมที่สอง

ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ 10_tabview_second_screen.py สร้าง ตัวเลขอุณหภูมิบนจอเป็นค่าที่ไฟล์นั้นคำนวณขึ้นเอง ไม่ใช่ผลการวัดจากเซนเซอร์

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

ทำอะไรได้ ทำไมมันสำคัญกับหน้าจอควบคุม
แบ่งพื้นที่เดิมเป็นหลายหน้า สลับด้วยการแตะ ของที่อยู่คนละแท็บไม่แย่งที่กัน เพราะไม่เคยอยู่บนจอพร้อมกัน
แถบแท็บบอกล่วงหน้าว่ามีอะไรอยู่บ้าง ต่างจากปุ่ม "หน้าถัดไป" ที่ซ่อนรายการไว้ในหัวคนเขียน
.add_tab("ชื่อ") คืนหน้า มาให้ใส่ใน parent= พิกัดของลูกนับจากมุมซ้ายบนของหน้า ไม่ใช่ของจอ

ราคาที่ต้องจ่าย — แท็บสามใบกินโควตาไป 4 แฮนเดิล ตั้งแต่ยังไม่มีอะไรอยู่ในนั้น คือตัว Tabview เองหนึ่ง บวกหน้าที่ add_tab() คืนมาอีกใบละหนึ่ง เอาเลขนี้ไปบวกกับงบใน 06_layout_budget.py ก่อนวางผัง

value= ของ Tabview คือความสูงของแถบแท็บ ไม่ใช่แท็บที่เปิดอยู่ — ตั้ง value=88 เพราะแถบแท็บคือของที่ต้องแตะ และ 88 พิกเซลคือขั้นต่ำของเป้าสัมผัส ค่าปริยายคือ 40 ซึ่งเตี้ยเกินไป

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เมื่อของไม่ได้เท่ากันทุกหัวข้อ — เมนูแบบเป็นชั้น

หน้าจอของ 11_menu_settings_tree.py: จอที่สองแบบที่สอง เมื่อของไม่ได้เท่ากันทุกหัวข้อ

ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด — แสดงหน้าจอที่ 11_menu_settings_tree.py สร้าง ค่าที่เห็นในเมนูมาจาก dict ในไฟล์ตัวอย่างเอง

แท็บเหมาะกับของที่เท่ากัน เมนูเหมาะกับของที่เป็นชั้น — "ตอนนี้ ทั้งวัน เกณฑ์" คือสามหัวข้อที่ใครมาก่อนมาหลังก็ได้ ส่วน "ตั้งค่า > เครือข่าย > วงที่ใช้" ไม่ใช่ ยัดโครงแบบหลังลงแท็บจะได้แท็บสิบใบที่ไม่มีใครหาของเจอ

ชิ้นส่วน ได้มาจาก ทำหน้าที่
หน้า menu.add_page("ชื่อ") หน้าแรกที่สร้างคือหน้าที่เมนูเปิดให้เอง
กลุ่ม page.section() กล่องจัดกลุ่มแถว — ไม่ใช่หน้า
แถว section.row("ข้อความ") บรรทัดที่แตะได้
เส้นเชื่อม row.opens(page) หัวใจของ widget ตัวนี้ ขาดบรรทัดนี้เมนูจะแตะได้แต่ไม่ไปไหน

ข้อจำกัดที่ต้องออกแบบรอบมัน — เมนูไม่ส่ง event กลับมาเลยสักตัว ทั้งตัวเมนูและแถวของมันไม่ได้ลงทะเบียน callback ไว้ใน ui_widget_mgr.c การแตะแถวถูกจัดการจบภายใน LVGL แปลว่า โปรแกรมเราไม่มีทางรู้ว่าผู้ใช้เปิดหน้าไหนอยู่

เมนูจึงเป็น "ที่ให้คนเดินดู" ไม่ใช่ "ที่ให้โปรแกรมรับคำสั่ง" — ของที่ต้องรับคำสั่งยังต้องเป็น Button Switch หรือ Spinbox ที่วางไว้ต่างหาก นี่คือเหตุผลที่หน้าจอในตัวอย่างมีการ์ดสรุปอยู่ข้าง ๆ เมนูเสมอ

product ต่างจาก demo ตรงนี้เอง — demo ฝังค่าไว้ในโค้ดแล้วแฟลชใหม่เมื่อต้องเปลี่ยน ส่วน product มีหน้าตั้งค่าให้คนหน้างานเปิดดูได้ว่าตอนนี้เครื่องใช้ค่าอะไรอยู่ โดยไม่ต้องมีใครเปิดซอร์สให้ดู

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

หน้าจอของทุกไฟล์ในชุดบทเรียนนี้ (1/3)

หน้าจอของ 01_first_widgets.py: widget ตัวแรก และเหตุผลที่ต้องใส่ x กับ y ทุกครั้ง หน้าจอของ 02_event_types.py: เหตุการณ์หน้าตาเป็นอย่างไร และใครส่งอะไร หน้าจอของ 03_switch_matches_led.py: จอกับไฟจริงต้องพูดตรงกันเสมอ

01 widget ตัวแรก และเหตุผลที่ต้องใส่ x กับ y ทุกครั้ง · 02 เหตุการณ์หน้าตาเป็นอย่างไร และใครส่งอะไร · 03 จอกับไฟจริงต้องพูดตรงกันเสมอ
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

หน้าจอของทุกไฟล์ในชุดบทเรียนนี้ (2/3)

หน้าจอของ 04_seg7_takes_text.py: Seg7 รับได้ทั้งสองทาง แต่ให้ผลไม่เท่ากัน หน้าจอของ 05_sound_feedback.py: เสียงตอบรับตอนแตะปุ่ม หน้าจอของ 06_layout_budget.py: พื้นที่ 792x398 กับงบ widget: งบของคอร์ส 32 ตัว (เพดานเฟิร์มแวร์ 64)

04 Seg7 กินข้อความ ไม่ใช่กินตัวเลข · 05 เสียงตอบรับตอนแตะปุ่ม · 06 พื้นที่ 792x398 และงบของคอร์ส 32 ตัว (เพดานเฟิร์มแวร์ 64) · ภาพ 06 ใช้พอดี 32 ตัว พิมพ์ "งบ 32 (เพดาน 64)" และลองสร้างตัวที่ 33 ให้ดู
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

หน้าจอของทุกไฟล์ในชุดบทเรียนนี้ (3/3)

หน้าจอของ 07_find_move_hide_delete.py: จัดการ widget ที่สร้างไปแล้ว หน้าจอของ 08_dropdown_textarea.py: อีกสามชนิดที่รับอินพุตได้ และค่าที่ถามกลับได้จริง

07 จัดการ widget ที่สร้างไปแล้ว · 08 อีกสามชนิดที่รับอินพุตได้ และค่าที่ถามกลับได้จริง
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

อ้างอิงและเครดิต

เอกสารของผู้ผลิตและซอฟต์แวร์

วิดีโอ

ภาพ — วงจร User LEDs มาจากคู่มือคิต รูปที่ 78 (หน้า 89) ใช้เพื่อการเรียนการสอน · ภาพถ่ายจากภายนอกทั้งเจ็ดภาพเป็น Wikimedia Commons ภายใต้ CC0 / CC BY / CC BY-SA / สาธารณสมบัติ เครดิตอยู่ใต้ภาพแต่ละใบบนสไลด์ · ภาพหน้าจอบอร์ดสี่ภาพเป็นของผู้สอนเอง ถ่ายจาก Eva Kit ขณะรันไฟล์ใน โฟลเดอร์ examples/ ของบทเรียน 2.4–2.6 · ไดอะแกรมอื่นทุกภาพวาดใหม่เป็น inline SVG ไม่ได้คัดลอกจากเอกสารผู้ผลิต

ตัวอย่างโค้ด — ชุดประจำบทเรียนอยู่ที่ โฟลเดอร์ examples/ ของบทเรียน 2.4–2.6 (17 ไฟล์ · หมายเลข 07 กับ 08 เพิ่มเข้ามาเพื่อให้ทุกฟังก์ชันของ ui ฝั่งอินพุตมีที่ให้ลงมือจริง) · ตัวอย่างต่อยอดหนึ่งไฟล์คือ 07_hold_to_confirm.py · ชุดเสียงสี่ไฟล์ที่เคยอยู่ใน examples/usecase/ หมายเลข 08 ถึง 11 ถูกถอดออกจากคลังเมื่อ 14 ส.ค.

ตัวเลขที่สืบกลับได้ — คิววงแหวน 16 ช่อง ใส่ได้จริง 15 และทิ้งของใหม่เมื่อเต็ม (ui_widget_mgr.c:2687-2690, UI_EVENT_RING_SIZE ใน ipc_ui_protocol.h:676) · หยิบได้ครั้งละ 8 (UI_MAX_EVENTS_PER_POLL) · เพดานเฟิร์มแวร์ 64 widget (UI_MAX_WIDGETS, ipc_ui_protocol.h:675; งบของคอร์ส 32) · พื้นที่ปริยาย 792 × 398 (ui_widget_defaults.h) · เพดานข้อความ 126 ไบต์ทั้งตอนสร้างและตอน .text() (IPC_DATA_MAX_LEN ลบสอง) · ui.tone รับตำแหน่ง 1-4 ตัว ปริยาย WAVE_SQUARE / 100 / 150 ms (modui.c:1741-1760) · เสียงสำเร็จรูป 21 ตัว รูปคลื่น 4 แบบ (ตารางค่าคงที่ modui.c:1928-1953)

พฤติกรรมของ ui, gpio และกฎเหล็กห้าข้อ ตรวจจากซอร์สโค้ด KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw และ BENTO-TESAIoT-libraries โดยตรง แล้วให้ทีมตรวจอิสระอีกชุดไล่หักล้างทีละข้อ

ทุกตัวเลขบนสไลด์ชุดนี้สืบกลับไปที่เอกสารต้นทางหรือซอร์สโค้ดได้ ถ้าเจอที่ไม่ตรง บอกผู้สอนได้เลย

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

กล่องคือที่อยู่ ไม่ใช่ของที่ตอบได้

หน้าจอของ 14_container_coordinates.py: กล่องที่คอนเทนเนอร์คืนมา และระบบพิกัดของมัน
ภาพจากตัวจำลอง Eva Kit (KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw/sim) ซึ่งรัน ipc_ui.c กับ ui_widget_mgr.c ตัวจริงเดียวกับบอร์ด บนพื้นที่วาด 800x398 · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ 14_container_coordinates.py · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

parent= ทำสองอย่างพร้อมกัน และข้อที่สองคือข้อที่ทำให้คนงง

ผลที่ได้
จัดกลุ่ม ย้ายกล่อง ลูกย้ายตาม ลบกล่อง ลูกหายไปด้วย
เปลี่ยนระบบพิกัด x=0, y=0 ของลูก คือมุมซ้ายบนของกล่อง ไม่ใช่ของจอ

ป้ายสามใบในภาพเขียนข้อความเดียวกันและสั่งพิกัดเดียวกันทั้งสามใบ แต่ไปโผล่คนละที่ เพราะอยู่คนละกล่อง

กล่องกินแฮนเดิลของตัวเอง จากโควตา (งบคอร์ส 32 · เพดานเฟิร์มแวร์ 64) — กล่องเปล่าที่ทำหน้าที่แค่จัดกลุ่ม ยังต้องจ่ายหนึ่งใบ

เครื่องมือตรวจพิกัด tools/check_overlap.py ของหลักสูตรต้นฉบับ (ไม่ได้อยู่ในรีโพนี้) ข้าม widget ที่มี parent= แล้วนับให้เห็นว่าข้ามไปกี่ตัว เพราะมันอ่านพิกัดจากซอร์สและไม่รู้ว่าลูกอยู่คนละระบบพิกัด — จอที่ทำด้วยกล่อง ต้องเปิดภาพดูเอง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

กดค้าง — สิ่งที่ clicked บอกไม่ได้

หน้าจอของ 15_press_and_hold.py: ปุ่มที่ต้องกดค้าง และเหตุการณ์ที่ clicked บอกไม่ได้

ภาพจากตัวจำลอง Eva Kit (KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw/sim) ซึ่งรัน ipc_ui.c กับ ui_widget_mgr.c ตัวจริงเดียวกับบอร์ด บนพื้นที่วาด 800x398 · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ 15_press_and_hold.py · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

clicked มาถึงตอน ปล่อยนิ้วแล้ว ปุ่มเร่งที่ต้องเดินขึ้นเรื่อย ๆ ระหว่างกดค้าง จึงทำด้วย clicked ไม่ได้เลย

ชนิด มาถึงเมื่อไร
pressed นิ้วแตะลง — เริ่มทำงานได้ตรงนี้
long_pressed กดค้างเกิน 400 มิลลิวินาที — มาครั้งเดียว
long_pressed_repeat ยังกดอยู่ ย้ำทุก 100 มิลลิวินาที · value คือจำนวนครั้งที่ย้ำมาตั้งแต่ poll() รอบที่แล้ว
press_lost ยังกดอยู่ แต่เลื่อนนิ้วออกนอกปุ่มแล้ว
released ปล่อยแล้ว มาถึงเสมอ แม้ปล่อยนอกปุ่ม

ทั้งห้าชนิดต้องขอก่อนด้วย .listen() ค่าตั้งต้นคือเงียบ — ปุ่มที่ไม่มีใครขอรับ จะไม่ส่งอะไรลงคิวเลยแม้แต่ใบเดียว

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

ถามตัวเองว่า "ต้องรู้ตอนไหน" ก่อนเลือกชนิด — ตอนเริ่ม ตอนกำลังทำ หรือตอนจบ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ปัดนิ้ว เลื่อนรายการ และช่องที่ถูกเลือก

หน้าจอของ 16_swipe_scroll_focus.py: ปัดนิ้ว เลื่อนรายการ และช่องไหนกำลังถูกเลือก

ภาพจากตัวจำลอง Eva Kit (KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw/sim) ซึ่งรัน ipc_ui.c กับ ui_widget_mgr.c ตัวจริงเดียวกับบอร์ด บนพื้นที่วาด 800x398 · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ 16_swipe_scroll_focus.py · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด
ชนิด ใครส่ง value บอกอะไร
gesture ของที่ไม่เลื่อนตัวเอง เช่น ui.Panel ทิศที่ปัด ui.DIR_LEFT และพวกเดียวกัน
scroll_begin / scroll_end ของที่เลื่อนตัวเองได้จริง เช่น ui.List —
focused / defocused widget ที่รับสัมผัส —

gesture จะไม่ถูกส่งระหว่างที่มีการเลื่อนเกิดขึ้น — พอ LVGL ตัดสินใจว่านิ้วนี้กำลังเลื่อนเนื้อหาของ widget ตัวใดตัวหนึ่ง มันจะถือว่านิ้วนั้น "ถูกจองแล้ว" และไม่แปลงเป็นการปัดสั่งงาน แผ่นสำหรับปัดจึงต้องเป็นของที่ไม่เลื่อนตัวเอง อย่าง ui.Panel ส่วน scroll_begin / scroll_end ต้องขอจากของที่เลื่อนได้จริง — ในตัวอย่างนี้คือ ui.List ที่มีรายชื่อยาวกว่ากรอบ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ปัดนิ้ว เลื่อนรายการ และช่องที่ถูกเลือก (ต่อ)

ข้อยกเว้นที่ต้องรู้ — ui.Roller ไม่ส่ง scroll_* เลย วงล้อดู "เลื่อนได้" แต่ในสายตา LVGL มันไม่ใช่ของที่เลื่อนได้ ตอนสร้างมันสั่ง lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE) ใส่ตัวเอง แล้วขยับป้ายข้างในด้วยมือ scroll_begin / scroll_end จึงไม่มีวันมาถึงมัน สิ่งที่วงล้อรายงานคือ value_changed ตอนตัวเลือกเปลี่ยน

focused ใช้ตอบว่า "ตอนนี้ผู้ใช้อยู่ที่ช่องไหน" — ฟอร์มหลายช่องผูกแป้นพิมพ์ตามช่องที่ถูกแตะได้ด้วยเหตุการณ์นี้ตัวเดียว โดยไม่ต้องมี lv_group และไม่ต้องมีคีย์บอร์ดจริง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เหตุการณ์เดินทางมาถึงเรายังไง

LVGL เห็นนิ้ว  →  CM55 เรียก callback  →  คิว 16 ช่อง  →  ui.poll() ตักได้ 8  →  ลูปของเรา

สี่ขั้นนี้อธิบายเกือบทุกอาการที่ผู้เรียนจะเจอในชุดบทเรียนนี้

อาการ ขั้นที่เป็นเหตุ
กดปุ่มแล้วไม่มีอะไรเกิดขึ้น ลูปไม่ได้เรียก ui.poll() — คิวเต็มแล้วของใหม่ถูกทิ้ง
ปุ่มหน่วง ๆ กดไม่ค่อยติด time.sleep_ms() นาน ระหว่างหลับไม่มีใครตักคิว
แตะรัว ๆ แล้วบางครั้งหาย ตักได้ครั้งละ 8 ที่เหลือรอรอบหน้า ถ้าลูปช้าก็ไล่ไม่ทัน

คิวมี 16 ช่อง และของใหม่คือตัวที่ถูกทิ้งเมื่อเต็ม ไม่ใช่ของเก่า — เพราะของเก่าอาจเป็นเหตุการณ์ที่โปรแกรมกำลังจะประมวลผลอยู่พอดี

ui.poll() ไม่ใช่การ "ถามว่ามีอะไรไหม" แต่เป็นการ "ตักออกจากคิว" — ไม่ตัก คิวก็เต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ทำไมต้องขอก่อน — .listen()

เหตุการณ์สามชนิดมาถึงเสมอโดยไม่ต้องขอ ส่วนอีกสิบสองชนิด เงียบจนกว่าจะขอ

ชนิด ต้องขอไหม
มาเอง clicked value_changed toggled ไม่ต้อง
ต้องขอ pressed released press_lost long_pressed long_pressed_repeat ready cancel focused defocused scroll_begin scroll_end gesture .listen("...")
btn.listen("pressed", "long_pressed_repeat", "released")

เหตุผลคือคิว 16 ช่องนั้นเอง — นิ้วที่แตะจอหนึ่งครั้ง ทำให้เกิดได้ทั้ง pressed focused defocused released และถ้ามีการเลื่อนด้วยก็ scroll_begin scroll_end ตามมาอีก นิ้วเดียว หลายใบ ถ้าทุก widget บนจอส่งทุกชนิดตลอดเวลาโดยไม่มีใครอ่าน คิวสิบหกช่องเต็มได้ในไม่กี่การแตะ แล้วเหตุการณ์ที่เราสนใจจริงจะถูกทิ้งตั้งแต่ยังไม่ถึงมือเรา

ค่าตั้งต้นจึงเป็นเงียบ แล้วให้เราบอกว่าจะอ่านอะไร — widget ที่ไม่เคยเรียก .listen() เลย จะไม่ถูกติดตัวดักเหตุการณ์ให้ด้วยซ้ำ

ขอเท่าที่จะอ่านจริง ไม่ขอเผื่อไว้ — ทุกชนิดที่ขอเพิ่มคือช่องในคิวที่ถูกใช้จริง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ถามให้ถูกจังหวะ

clicked มาถึงตอน ปล่อยนิ้วแล้ว ปุ่มเร่งที่ต้องเดินขึ้นระหว่างกดค้าง จึงทำด้วย clicked ไม่ได้เลย

อยากรู้ว่า ใช้ชนิด
เริ่มแตะแล้ว pressed
ยังกดค้างอยู่ long_pressed_repeat — value คือจำนวนครั้งที่ย้ำมาตั้งแต่ poll() รอบที่แล้ว
นิ้วเลื่อนออกนอกปุ่มแล้ว press_lost — สั่งหยุดตรงนี้
จบแล้ว released — มาถึงเสมอ แม้ปล่อยนอกปุ่ม
ผู้ใช้พิมพ์เสร็จ ready
ตอนนี้อยู่ที่ช่องไหน focused

ถามตัวเองว่า "ต้องรู้ตอนไหน" ก่อนเลือกชนิด — ตอนเริ่ม ตอนกำลังทำ หรือตอนจบ

gesture ไม่ถูกส่งระหว่างที่มีการเลื่อนเกิดขึ้น และ ui.Roller ไม่ส่ง scroll_* เลยเพราะ LVGL ไม่ถือว่ามันเป็นของที่เลื่อนได้ — สองข้อนี้เจอจากการปัดจอจริงก่อน แล้วจึงไปอ่าน lv_roller.c เพื่อยืนยันว่าทำไม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0