ต้องทำในบทเรียน · เปิดตามลำดับนี้ ทั้งชุดราว 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 เพราะจะทำให้แตะอย่างอื่นไม่ได้ทั้งช่วง
สามไฟล์แรกคือของที่ต้องเปิดจริงในบทเรียน ตารางล่างเปิดเฉพาะตอนเจออาการนั้น
ฝั่งระบบสมองกลฝังตัว
สถาปัตยกรรม 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 แทนที่จะมาจากนิ้ว
วันนี้เราได้:
สร้าง 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 ที่เขียนวันนี้ จะเป็นโครงเดิมของทุกโปรแกรมที่เหลือในคอร์สนี้
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)
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 อยู่บนโมดูล)
ข้อมูลที่คู่กันให้เรียงให้ตรงกัน แล้วดัชนีเดียวจะพาเราไปได้ทั้งโปรแกรม
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]
ทำไมเลิกใช้ ui.Switch ตัวเดียวคุมสามสี — สวิตช์คือปุ่มสลับ และปุ่มสลับบอกไม่ได้ว่าตอนนี้อยู่สถานะไหน คนกดต้องอ่านจากที่อื่นแล้วเดา แผงควบคุมจริงจึงแยก ปุ่มเปิด กับ ปุ่มปิด เสมอ ปุ่ม "เปิด" ที่กดซ้ำสิบครั้งได้ผลเดียวกับกดครั้งเดียว ซึ่งสำคัญมากตอนคนกดซ้ำเพราะไม่แน่ใจ
ไฟสถานะแยกจากปุ่ม ไม่ใช่ให้ปุ่มเปลี่ยนสีเอง เพราะสองอย่างนี้คนละหน้าที่ — ไฟตอบว่า "ตอนนี้เป็นยังไง" ปุ่มตอบว่า "สั่งอะไรได้" และไฟผ่านการทดสอบขาวดำ ส่วนปุ่มที่เปลี่ยนสีไม่ผ่าน · ปุ่มสูง 88 และห่างกัน 32 ตามระยะนิ้วจริง ปุ่มเปิดใช้สีสถานะ ok 0x30A46C ปุ่มปิดใช้สีปุ่มรอง 0x3A4150
ทุกอย่างในแถวอ้างด้วย
iตัวเดียว — ข้อความ สี ไฟ ปุ่ม และดวงจริงLED_IDX[i]
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 ตอนรัน
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) # เริ่มจากที่รู้แน่
ลูปสามบรรทัดสุดท้ายมีความหมายมากกว่าที่เห็น — เราไม่ได้ สมมติ ว่าไฟดับตอนเริ่ม แต่เรา สั่งให้มันดับ แล้วให้จอรายงานตามนั้น ถ้าโปรแกรมก่อนหน้าทิ้งไฟติดค้างไว้ บรรทัดนี้จะเก็บกวาดให้เอง
โปรแกรมที่ดีไม่เชื่อสภาพเริ่มต้นที่ตัวเองไม่ได้เป็นคนกำหนด
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)
set_led(on_ids.index(h), True) สั่งเปิด ไม่ใช่สั่งสลับ — คำสั่งที่ระบุปลายทางแบบนี้เรียกว่า idempotent กดซ้ำกี่ครั้งก็ได้ผลเดิม ต่างจากคำสั่งสลับที่ผลขึ้นกับสถานะก่อนหน้า ซึ่งเป็นสิ่งที่คนกดมองไม่เห็น · ทุกปุ่มส่ง 'clicked' เหมือนกันหมด จึงกรอง type ทิ้งรอบเดียวข้างบน แล้วแยกด้วย handle อย่างเดียว — สั้นกว่าและพลาดยากกว่าการเช็กควบสองอย่างทุกกิ่ง
ปิดทั้งหมด เป็นกิ่งเดียวที่ไม่ลงมือทันที มันเปิดกล่องยืนยันก่อน เพราะแตะทีเดียวเปลี่ยนของจริงสามชิ้นพร้อมกัน และคำยืนยันบอก สิ่งที่จะเกิด ว่า "ไฟทุกสีจะดับพร้อมกัน" ไม่ใช่ถามลอย ๆ ว่า "แน่ใจไหม"
while Trueที่ไม่มีtime.sleep_ms()คือบั๊กที่ไม่แสดงตัวเป็น error แต่ทำให้ทั้งระบบช้าลง
ท่า 1 ล้างจอ + หัวเรื่อง พิสูจน์ว่าช่องทางถึงจอใช้ได้ ถ้าหัวเรื่องยังไม่ขึ้น อย่าเพิ่งเขียนอะไรต่อ
ท่า 2 แถวของแต่ละดวง มาก่อนตรรกะ เพราะ "มองเห็น" ง่ายที่สุด ตอนนี้กดยังไม่มีอะไรเกิด — และนั่นถูกต้องแล้ว
ท่า 3 คำสั่งทั้งชุดกับกล่องยืนยัน สร้างพร้อมกันตรงนี้แล้วซ่อนไว้ ไม่ใช่ไปสร้างตอนคนกด การสร้างของตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด
ท่า 4 set_led() มาก่อนลูป เพื่อให้ทดสอบตรง ๆ ได้ด้วย set_led(0, True) บรรทัดเดียว ไฟติดแปลว่าฝั่งฮาร์ดแวร์เรียบร้อย
ท่า 5 ตั้งต้นให้ดับ มาก่อนลูป เพราะ set_led() ต้องเรียกได้ตั้งแต่บรรทัดแรกที่ทำงาน และทุกการรันต้องเริ่มจากจุดเดียวกัน
ท่า 6 event loop มาสุดท้าย เพราะมันเป็นแค่ "คนเดินสาร" ถ้าห้าท่าบนถูกหมด ท่านี้จะสั้นและตรงไปตรงมา
ถ้าเขียน event loop ตั้งแต่แรกแล้วมันเงียบ เราจะไม่รู้เลยว่าพังที่จอ ที่ไฟ หรือที่ตรรกะ
คำถามคิดต่อ: ถ้าคำสั่งเปิดไฟมาจากอินเทอร์เน็ตแทนนิ้ว โค้ดส่วนไหนต้องเปลี่ยน · ส่วนไหนไม่ต้องเปลี่ยนเลย · แล้วเราจะรู้ได้อย่างไรว่าคำสั่งจากเน็ตมาถึงจริง



สามภาพนี้ตอบคำถามเดียวกับที่แผงของเราต้องตอบ: การตอบกลับผู้ใช้ไม่จำเป็นต้องเกิดบนหน้าจอเสมอไป และเมื่อของสำคัญเข้ามา มันต้องแซงทุกอย่างที่แสดงอยู่
ทั้งสี่มุมเจอโจทย์เดียวกันหมด: มีคนสั่งได้หลายทาง แต่ความจริงต้องมีชุดเดียว
ทำไมคลิปนี้เกี่ยวกับการบ้านข้อ 4
ข้อ 4 ให้เอาปุ่มผู้ใช้จริงบนบอร์ด (gpio.button(0) — เรียกด้วยชื่อจาก .name() ไม่ใช่ป้ายบนแผ่นวงจร) มาทำงานร่วมกับปุ่มบนจอในลูปเดียวกัน คลิปนี้ทบทวนว่าทำไมปุ่มกลไกกดครั้งเดียวถึงกลายเป็นหลายเหตุการณ์
ปุ่มบนจอไม่มีปัญหานี้เพราะ CM55 จัดการให้แล้ว — ปุ่มจริงต้องทำเอง
อ่านต่อสำหรับคนอยากรู้ลึก
time.ticks_ms() / ticks_diff() ที่ใช้จับจังหวะลูป — MicroPython docs: https://docs.micropython.org/en/latest/library/time.htmlปุ่มบนจอไม่ต้อง debounce เพราะมีคนทำให้แล้ว ปุ่มจริงต้องทำเอง — จำความต่างนี้ไว้ตอนทำข้อ 4

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

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) แล้วหรี่ ไม่ใช่หาย — ตั้งใจให้เป็นแบบนั้น ไฟแผงควบคุมที่หายไปตอนดับ แย่กว่าไฟที่หรี่ลง เพราะคนดูแยกไม่ออกว่าดับหรือจอเสีย
กติกาที่แผงจริงใช้ — ให้ติดทีละดวง ถ้าติดพร้อมกันสองดวงตอนคาบเกี่ยว คนอ่านไม่ออกว่าตกลงสถานะอะไร

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"กว้างไม่เท่ากัน ตารางจริงจัดคอลัมน์ให้

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 ตัวใดวาดไม่ออก ชุดทดสอบล้มก่อนที่ห้องเรียนจะเจอ

10_tabview_second_screen.py สร้าง ตัวเลขอุณหภูมิบนจอเป็นค่าที่ไฟล์นั้นคำนวณขึ้นเอง ไม่ใช่ผลการวัดจากเซนเซอร์ในงานจริง หน้าจอเครื่องหนึ่งเครื่องมีของต้องโชว์มากกว่าที่ตาคนรับไหวเสมอ — ค่าปัจจุบัน ค่าย้อนหลัง เกณฑ์ที่ตั้งไว้ สถานะเครือข่าย ประวัติการแจ้งเตือน ทีมที่ยัดทุกอย่างลงหน้าเดียวจะได้จอที่ "มีข้อมูลครบและอ่านไม่ได้เลย" ซึ่งแย่กว่าจอที่มีของน้อยกว่า
| ทำอะไรได้ | ทำไมมันสำคัญกับหน้าจอควบคุม |
|---|---|
| แบ่งพื้นที่เดิมเป็นหลายหน้า สลับด้วยการแตะ | ของที่อยู่คนละแท็บไม่แย่งที่กัน เพราะไม่เคยอยู่บนจอพร้อมกัน |
| แถบแท็บบอกล่วงหน้าว่ามีอะไรอยู่บ้าง | ต่างจากปุ่ม "หน้าถัดไป" ที่ซ่อนรายการไว้ในหัวคนเขียน |
.add_tab("ชื่อ") คืนหน้า มาให้ใส่ใน parent= |
พิกัดของลูกนับจากมุมซ้ายบนของหน้า ไม่ใช่ของจอ |
ราคาที่ต้องจ่าย — แท็บสามใบกินโควตาไป 4 แฮนเดิล ตั้งแต่ยังไม่มีอะไรอยู่ในนั้น คือตัว Tabview เองหนึ่ง บวกหน้าที่ add_tab() คืนมาอีกใบละหนึ่ง เอาเลขนี้ไปบวกกับงบใน 06_layout_budget.py ก่อนวางผัง
value= ของ Tabview คือความสูงของแถบแท็บ ไม่ใช่แท็บที่เปิดอยู่ — ตั้ง value=88 เพราะแถบแท็บคือของที่ต้องแตะ และ 88 พิกเซลคือขั้นต่ำของเป้าสัมผัส ค่าปริยายคือ 40 ซึ่งเตี้ยเกินไป
กติกาข้อเดียวที่ห้ามลืม — สิ่งที่ต้องเห็นตลอดเวลาห้ามอยู่ในแท็บ สถานะเครือข่าย สัญญาณเตือน และปุ่มหยุดฉุกเฉิน ต้องอยู่นอกแท็บเสมอ ไม่งั้นมันจะหายไปตอนที่ผู้ใช้กำลังดูแท็บอื่น ซึ่งคือตอนที่มันสำคัญที่สุดพอดี

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 มีหน้าตั้งค่าให้คนหน้างานเปิดดูได้ว่าตอนนี้เครื่องใช้ค่าอะไรอยู่ โดยไม่ต้องมีใครเปิดซอร์สให้ดู



เอกสารของผู้ผลิตและซอฟต์แวร์
time (ticks_ms / ticks_diff): https://docs.micropython.org/en/latest/library/time.html — เอกสาร upstream มี machine.PWM/ADC/Timer ที่ พอร์ตของเรายังไม่มีวิดีโอ
ภาพ — วงจร 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 โดยตรง แล้วให้ทีมตรวจอิสระอีกชุดไล่หักล้างทีละข้อ
ทุกตัวเลขบนสไลด์ชุดนี้สืบกลับไปที่เอกสารต้นทางหรือซอร์สโค้ดได้ ถ้าเจอที่ไม่ตรง บอกผู้สอนได้เลย

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=แล้วนับให้เห็นว่าข้ามไปกี่ตัว เพราะมันอ่านพิกัดจากซอร์สและไม่รู้ว่าลูกอยู่คนละระบบพิกัด — จอที่ทำด้วยกล่อง ต้องเปิดภาพดูเอง

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 — ซึ่งคือใบที่สั่งให้หยุด
ถามตัวเองว่า "ต้องรู้ตอนไหน" ก่อนเลือกชนิด — ตอนเริ่ม ตอนกำลังทำ หรือตอนจบ

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 ที่มีรายชื่อยาวกว่ากรอบ
ข้อยกเว้นที่ต้องรู้ — ui.Roller ไม่ส่ง scroll_* เลย วงล้อดู "เลื่อนได้" แต่ในสายตา LVGL มันไม่ใช่ของที่เลื่อนได้ ตอนสร้างมันสั่ง lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE) ใส่ตัวเอง แล้วขยับป้ายข้างในด้วยมือ scroll_begin / scroll_end จึงไม่มีวันมาถึงมัน สิ่งที่วงล้อรายงานคือ value_changed ตอนตัวเลือกเปลี่ยน
focused ใช้ตอบว่า "ตอนนี้ผู้ใช้อยู่ที่ช่องไหน" — ฟอร์มหลายช่องผูกแป้นพิมพ์ตามช่องที่ถูกแตะได้ด้วยเหตุการณ์นี้ตัวเดียว โดยไม่ต้องมี lv_group และไม่ต้องมีคีย์บอร์ดจริง
LVGL เห็นนิ้ว → CM55 เรียก callback → คิว 16 ช่อง → ui.poll() ตักได้ 8 → ลูปของเรา
สี่ขั้นนี้อธิบายเกือบทุกอาการที่ผู้เรียนจะเจอในชุดบทเรียนนี้
| อาการ | ขั้นที่เป็นเหตุ |
|---|---|
| กดปุ่มแล้วไม่มีอะไรเกิดขึ้น | ลูปไม่ได้เรียก ui.poll() — คิวเต็มแล้วของใหม่ถูกทิ้ง |
| ปุ่มหน่วง ๆ กดไม่ค่อยติด | time.sleep_ms() นาน ระหว่างหลับไม่มีใครตักคิว |
| แตะรัว ๆ แล้วบางครั้งหาย | ตักได้ครั้งละ 8 ที่เหลือรอรอบหน้า ถ้าลูปช้าก็ไล่ไม่ทัน |
คิวมี 16 ช่อง และของใหม่คือตัวที่ถูกทิ้งเมื่อเต็ม ไม่ใช่ของเก่า — เพราะของเก่าอาจเป็นเหตุการณ์ที่โปรแกรมกำลังจะประมวลผลอยู่พอดี
ui.poll()ไม่ใช่การ "ถามว่ามีอะไรไหม" แต่เป็นการ "ตักออกจากคิว" — ไม่ตัก คิวก็เต็ม
.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() เลย จะไม่ถูกติดตัวดักเหตุการณ์ให้ด้วยซ้ำ
ขอเท่าที่จะอ่านจริง ไม่ขอเผื่อไว้ — ทุกชนิดที่ขอเพิ่มคือช่องในคิวที่ถูกใช้จริง
clicked มาถึงตอน ปล่อยนิ้วแล้ว ปุ่มเร่งที่ต้องเดินขึ้นระหว่างกดค้าง จึงทำด้วย clicked ไม่ได้เลย
| อยากรู้ว่า | ใช้ชนิด |
|---|---|
| เริ่มแตะแล้ว | pressed |
| ยังกดค้างอยู่ | long_pressed_repeat — value คือจำนวนครั้งที่ย้ำมาตั้งแต่ poll() รอบที่แล้ว |
| นิ้วเลื่อนออกนอกปุ่มแล้ว | press_lost — สั่งหยุดตรงนี้ |
| จบแล้ว | released — มาถึงเสมอ แม้ปล่อยนอกปุ่ม |
| ผู้ใช้พิมพ์เสร็จ | ready |
| ตอนนี้อยู่ที่ช่องไหน | focused |
ถามตัวเองว่า "ต้องรู้ตอนไหน" ก่อนเลือกชนิด — ตอนเริ่ม ตอนกำลังทำ หรือตอนจบ
gestureไม่ถูกส่งระหว่างที่มีการเลื่อนเกิดขึ้น และui.Rollerไม่ส่งscroll_*เลยเพราะ LVGL ไม่ถือว่ามันเป็นของที่เลื่อนได้ — สองข้อนี้เจอจากการปัดจอจริงก่อน แล้วจึงไปอ่านlv_roller.cเพื่อยืนยันว่าทำไม