lcd กับ ui ส่งข้อความและตัวเลขของทีมขึ้นจอได้ปลายทางของวันนี้: จอบอร์ดขึ้นชื่อทีมเรา ชื่อสมาชิกไล่ทีละคน และบรรทัดสีเขียวว่าสำเร็จ
คาบนี้วัดกันที่ "เล่นเป็น + อธิบายได้" ไม่ใช่ "เขียนโค้ดยาว" — เปิดบอร์ดได้เลยตั้งแต่สไลด์หน้า
เปิดบอร์ด รอจอติด แล้วอยู่ที่หน้า Home ก่อน อย่าเพิ่งกดเข้าเมนูไหน
ลองทีละอย่างแล้วสังเกตแผง Sensor Live ทางขวาของจอ:

ยังไม่มีโค้ดของเราสักบรรทัด แต่ค่าพวกนี้วิ่งอยู่แล้ว — เพราะเฟิร์มแวร์อ่านเซนเซอร์ให้ตลอดเวลา
Controls (Eva Kit เท่านั้น) — แตะวงกลมสีบนจอ แล้วมอง หลอด LED จริงบนบอร์ด ว่าติดตาม
นี่คือครั้งแรกที่น้อง ๆ เห็นว่า "แตะกระจก" ทำให้ "ของจริงเปลี่ยนสถานะ" ได้ (คาบ 3 กับ 4 เราจะสร้างหน้านี้เอง) · Dev Kit ไม่มีการ์ด Controls — ทีมที่ถือ Dev Kit ให้ข้ามไปเมนูถัดไปก่อน แล้วดูไฟจริงติดจากโค้ดตอนรัน examples/s01/11_lights_and_a_button.py↗ ในครึ่งหลังของคาบแทน
Sensor Dashboard — เขย่าบอร์ดเบา ๆ แล้วดูกราฟทางซ้ายกระเพื่อม เข็มทิศทางขวาหมุนตามการหันบอร์ด
(คาบ 7 กับ 8 เราจะสร้างกราฟและเข็มทิศแบบนี้เอง)
Smart Watch — ปัดนิ้วซ้าย-ขวาเพื่อเปลี่ยนหน้า มีทั้งหน้านาฬิกา สุขภาพ เพลง อากาศ
ตัวนับก้าวคำนวณจากเซนเซอร์ความเร่งตัวเดียวกับที่เราจะใช้ในคาบ 6 · เวลาบนหน้าปัดยังไม่ตรง เพราะบอร์ดตั้งนาฬิกาจากอินเทอร์เน็ตเท่านั้น และยังไม่ได้ต่อ WiFi (คาบหน้าต่อแล้วจะตรงเอง)

แผงหน้าปัดรถยนต์จริง เกจกลม สวิตช์ และวิทยุอยู่ครบในหน้าเดียว — คือสิ่งที่เมนู Sensor Dashboard บนบอร์ดกำลังเลียนแบบ ระหว่างเล่นให้เทียบดูว่า พอย้ายทุกอย่างขึ้นจอเดียวแล้ว อะไรหายไป จากการหมุนลูกบิดจริงบ้าง (ภาพ: Eric Friedebach, Wikimedia Commons, CC BY 2.0)
เล่นแล้วจดลง worksheet ทันที — ความจำจากการเล่นหายเร็วกว่าที่คิด
Audio Player (Eva Kit เท่านั้น — Dev Kit ไม่มีการ์ดนี้) — เครื่องเล่นเพลงจริงจาก SD card
ถ้าไม่ได้เสียบ SD card หน้านี้จะขึ้น Now Playing: No SD Card และบรรทัด สีแดง SD Card failed: step 2 (InitCard) — นี่ไม่ใช่บอร์ดเสีย ปุ่มเล่นและแถบเสียงยังอยู่ครบ ขาดแค่รายชื่อไฟล์
Wi-Fi Setting — สแกนหาเครือข่าย เลือก แล้วใส่รหัส
คาบนี้ยังไม่ต้องต่อ (คาบ 2 จะต่อด้วยโค้ดของเราเอง) แต่ให้กดเข้าไปดูว่าหน้าตาเป็นยังไง — เมนูนี้มีทั้งสองบอร์ด และอยู่ในเกณฑ์ผ่านของคาบ
BENTO Playground — หน้าที่สำคัญที่สุดสำหรับเรา
ทุกครั้งที่เราส่งโค้ดจากคอมมาที่บอร์ด ผลลัพธ์จะมาโผล่ที่หน้านี้ ถ้าไม่เปิดหน้านี้ค้างไว้ จะงงว่าทำไมส่งโค้ดแล้วไม่เห็นอะไร
TESAIoT Connectivity (Eva Kit เท่านั้น — Dev Kit ไม่มีการ์ดนี้) — แดชบอร์ดสถานะการเชื่อมต่อกับแพลตฟอร์ม IoT
ปลายทางของคาบ 10-11 อยู่ตรงนี้ กดเข้าไปดูไว้ก่อนได้ว่าเราจะไปจบที่ไหน — ทีมที่ถือ Dev Kit จะได้เห็นปลายทางเดียวกันผ่านโค้ดของตัวเองในคาบ 10-11
เมนู Playground คือประตูที่เราจะเดินผ่านทุกคาบตั้งแต่วันนี้เป็นต้นไป

examples/s01/15_one_number_many_faces.py↗ บน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ไม่ใช่ภาพวาด ไม่ใช่ mock-up · ภาพนี้เก่า ถ่ายตอนไฟล์ยังพิมพ์ว่า "จาก 32" ปัจจุบันไฟล์พิมพ์ "จาก 64" แล้ว รอถ่ายใหม่ui.list() นับได้ 24 ตัว จาก 64 — บอร์ดนับ widget ของตัวเองได้ · 64 คือเพดานของเฟิร์มแวร์ ส่วนงบที่คอร์สตั้งให้ตัวเองคือ 32 ต่อหน้า (คาบ 4 อธิบายว่าทำไม)ทั้งหมดนี้เขียนด้วย Python บนบอร์ด ไม่มี LVGL ไม่มี C สักบรรทัด — และวันนี้น้อง ๆ จะได้เขียนเอง
สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
อ่านเซนเซอร์ตลอดเวลา, วาดหน้าจอทุกเมนู, จัดการ IPC ระหว่างสองคอร์, ระบบไฟล์บนบอร์ด, รัน MicroPython, จัดการวิทยุ WiFi
สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่า จะเอาข้อมูลอะไร มาแสดง ยังไง และ ส่งไปไหนต่อ
วันนี้ 30% ของเราเล็กมาก คือแค่ "พิมพ์อะไรลงจอ" แต่กลไกเบื้องหลังเหมือนกันทุกคาบ
เราไม่ได้เรียนเขียนไดรเวอร์ เราเรียน ออกแบบระบบ AIoT โดยยืนบนไดรเวอร์ที่มีอยู่แล้ว
สามตัวนี้คือของที่จะพิมพ์เองครบทุกบรรทัดในครึ่งแรก ส่วนครึ่งหลังของคาบจะเปิดอีกห้าโมดูล — gpio sensors dsp mic machine — และ widget อีกสิบกว่าชนิด ให้เห็นว่ากล่องนี้มีอะไรอยู่จริงบ้าง ก่อนจะไปลงลึกทีละตัวในคาบต่อ ๆ ไป
print()ขึ้นที่คอนโซลฝั่งคอม ·lcd.print()ลงลิ้นชักบนบอร์ด ·ui.Labelขึ้นบนหน้าจอตรง ๆ — สามที่ คนละที่กัน
import lcd
import time
import ui
...
ui.screen() # ล้าง widget เดิมทิ้ง เริ่มจากจอเปล่าที่เรารู้แน่
time.sleep_ms(200)
...
say = ui.Label("กำลังพิมพ์ลงลิ้นชัก Console...", x=24, y=56,
color=COL_ACCENT, value=24)
...
ui.poll() # เคาะหนึ่งครั้ง ป้ายถึงจะโผล่ทันที
...
lcd.clear()
lcd.print("สวัสดี บอร์ด PSoC Edge")
กฎข้อเดียวที่ต้องจำวันนี้: สร้างหรือแก้ ui.* แล้วต้องเคาะ ui.poll() หนึ่งครั้ง ไม่งั้นจอจะนิ่งไปราวสองวินาทีจนกลไกกันเหนียวปลดล็อกเอง หลายทีมสรุปว่า "โค้ดพัง" ทั้งที่แค่ยังไม่ได้เคาะ (กติกาที่เหลือของ ui รอคาบ 4) · บรรทัดพวกนี้ตัดมาจาก examples/s01/01_first_line.py↗ ตรง ๆ — COL_ACCENT คือสีเน้น 0x4A9EFF ที่หัวไฟล์ประกาศไว้
value= บน ui.Label คือขนาดตัวอักษร ไม่ใช่ตัวเลขที่จะเอาไปแสดง — จุดนี้ทำคนสะดุดทุกรุ่น

lcd ครั้งแรก แต่เปิดรอไว้จะเห็นผลตั้งแต่วินาทีแรก)examples/s01/ แล้วกด Program to Devicelcd จะรออยู่จนกว่าจะกดสลับ สังเกตจุดแดงเล็ก ๆ ที่มุมปุ่มเมื่อมีข้อความรอพื้นที่วาดของเราคือ 792 x 398 พิกเซล และมุมขวาล่างราว 100x58 เป็นของปุ่ม Console ที่เฟิร์มแวร์จองไว้ — วาง widget ทับตรงนั้นแล้วจะกดเปิดลิ้นชักไม่ได้
ทีมละหนึ่งบอร์ด — สลับกันเป็นคนพิมพ์ทุกช่วง อย่าให้ใครนั่งดูอย่างเดียวทั้งคาบ
examples/s01/01_first_line.py↗ — หนึ่งบรรทัด สามปลายทางsay = ui.Label("กำลังพิมพ์ลงลิ้นชัก Console...", x=24, y=56,
color=COL_ACCENT, value=24)
ui.poll()
lcd.clear()
lcd.print("สวัสดี บอร์ด PSoC Edge")
lcd.print("หนึ่งบวกหนึ่งได้", 1 + 1)
print("บรรทัดนี้อยู่บนคอม ไม่ได้อยู่บนจอบอร์ด")
lcd.print() รับหลายค่าคั่นด้วยจุลภาคเหมือน print() ทุกประการ และเติมช่องว่างให้เอง ตัวเลขส่งได้เลยไม่ต้องแปลงก่อน
ท้ายไฟล์มี say.text(...) — แก้ข้อความบนป้ายเดิม ไม่ใช่วางป้ายใหม่ทับ ป้ายหนึ่งใบที่เปลี่ยนค่าได้ อ่านง่ายกว่าป้ายสิบใบที่กองทับกัน
ตาคุณ (ท้ายไฟล์): เพิ่มชื่อทีมเข้าไปในทั้งสามปลายทาง แล้วตอบว่าต้องเปิดดูที่ไหนบ้างถึงจะเห็นครบทั้งสามที่
จอเงียบมักแปลว่าเรายืนผิดหน้า ไม่ใช่โค้ดพัง — ไฟล์นี้มีไว้พิสูจน์ประโยคนั้นด้วยตาตัวเอง
examples/s01/02_markup_tags.py↗ — สีคือระดับ ไม่ใช่ของตกแต่งfor cls, text in REPORT:
# แท็กเปิดกับแท็กปิดต้องอยู่ในการเรียกครั้งเดียวกันเสมอ
lcd.print("<span class=" + cls + ">" + text + "</span>")
ห้าคลาสนี้คือ ห้าระดับความสำคัญ ไม่ใช่ห้าสีให้เลือกตามชอบ ชุดที่ 3 ในไฟล์จงใจทาข่าวดีเป็นสีแดง เพื่อให้เห็นว่าสีผิดทำร้ายคนอ่านก่อนที่เขาจะทันได้อ่านตัวหนังสือ
ฝั่งจอแกะแท็กทีละบรรทัด จบบรรทัดแล้วสถานะเริ่มใหม่หมด ลืมปิด </h2> ผลกระทบจำกัดอยู่ในบรรทัดนั้นบรรทัดเดียว
กับดัก: พิมพ์ชื่อคลาสผิด เช่น class=okay ไม่มี error ให้จับสักตัว บรรทัดนั้นแค่ออกมาเป็นสีปกติ — เจอบรรทัดที่ไม่มีสี ให้สงสัยชื่อคลาสก่อน
ตาคุณ: ใน REPORT มีบรรทัดหนึ่งติดระดับไว้ผิด หาให้เจอแล้วแก้ จากนั้นเพิ่มข่าวของทีมเองอีกหนึ่งบรรทัดพร้อมเลือกระดับให้มัน
เกณฑ์ตัดสินง่าย ๆ: บรรทัดนี้ทำให้คนที่เดินผ่านต้องลุกจากเก้าอี้ไหม ถ้าไม่ ก็ไม่ใช่
error
examples/s01/03_byte_limit.py↗ — เพดาน 127 ไบต์
examples/s01/03_byte_limit.py↗ — บันทึกโดยผู้สอนnbytes = len(text.encode()) # นับไบต์ ไม่ใช่นับตัวอักษร
seg.text(str(nbytes))
if nbytes > LIMIT:
seg.color(COL_BAD)
else:
seg.color(COL_OK)
len() ของสตริงนับ ตัวอักษร ส่วน len() ของ bytes นับ ไบต์ สองค่านี้ไม่เท่ากันเมื่อเป็นภาษาไทย เพราะไทยหนึ่งตัวกิน 3 ไบต์
เพดานที่ต้องจำมีสามตัว: lcd.print() ส่งได้ 127 ไบต์ ต่อครั้ง · ui.Label(...) ตอนสร้างพาได้ 126 ไบต์ · .text() พาได้ 126 ไบต์ เท่ากัน
ไทยจึงได้ราว 42 ตัวอักษร ต่อครั้ง เกินแล้วถูกตัดทิ้ง เงียบ ๆ ไม่มี error และ \n ท้ายบรรทัดหายไปด้วย บรรทัดถัดไปจึงมาต่อท้ายกันเละ
ui.Seg7 รับได้ทั้ง .text() และ .value() — แต่ .value(198) ขึ้น 198 เท่านั้น ถ้าอยากได้ 198.0 หรือ 0198 ต้องส่งเป็นข้อความ
ตาคุณ: ใส่ชื่อสมาชิกภาษาไทยลงใน ITEMS อีกหนึ่งแถว ทำนายก่อนรัน ว่าจะได้กี่ไบต์และจะเป็นเขียวหรือแดง แล้วรันเทียบกับที่ทำนาย
การตัดที่ต้องตัด ให้ตัดที่ ตัวอักษร ไม่ใช่ที่ไบต์ — ตัดกลางไบต์ของตัวอักษรหนึ่งตัวแล้วจอจะแสดงเป็นขยะ
examples/s01/04_console_drawer.py↗ — บทสรุปที่ไม่ต้องเปิดลิ้นชักCHECKS = [
("จอแสดงผล", True),
("ปุ่มบนบอร์ด", True),
("หลอด LED", True),
("การ์ด SD", False),
]
...
for name, ok in CHECKS:
if ok:
passed = passed + 1
...
lcd.print(name, "<span class=ok>ผ่าน</span>")
else:
...
lcd.print(name, "<span class=error>ไม่ผ่าน</span>")
seg.text(str(passed) + "-" + str(TOTAL))
bar.value(passed)
...
ui.poll()
คนหน้างานไม่ได้อยากอ่านรายงานสิบบรรทัด เขาอยากรู้บรรทัดเดียวว่า ผ่านกี่ข้อจากกี่ข้อ แล้วค่อยเจาะเฉพาะข้อที่ตก · lcd.console() ใช้กับสิ่งที่ "จัดหน้า" เช่นหัวเรื่องกับเส้นคั่น ส่วน lcd.print() ใช้กับ "ข้อมูล" — ทั้งคู่ลงลิ้นชักเดียวกัน
ป้ายสรุปสองใบสร้างไว้ตั้งแต่ต้น แล้วค่อยเขียนทับตอนรู้ผล ถ้าไปสร้างข้างใน if/else ทีหลัง จะได้ป้ายคนละใบวางที่พิกัดเดียวกันสองใบ ซึ่งบนจอจริงคือตัวหนังสือซ้อนกันอ่านไม่ออก แม้ตอนรันจะเข้าแค่ทางเดียวก็ตาม
ตาคุณ: แก้ CHECKS ให้ผ่านครบทุกข้อ ดูว่าการ์ดข้างบนเปลี่ยนไปยังไง แล้วเพิ่มรายการที่ห้า สังเกตว่าต้องแก้อะไรบ้างนอกจาก CHECKS (ใบ้: y ของแถวสุดท้ายยังอยู่ในจอ 398 พิกเซลหรือเปล่า)
โปรแกรมที่พูดเฉพาะตอนสำเร็จ จะเงียบสนิทตอนล้มเหลว ซึ่งคือจังหวะที่คนใช้อยากรู้ที่สุด
examples/s01/05_clear_and_refresh.py↗ — เขียนทับที่เดิม
examples/s01/05_clear_and_refresh.py↗ — บันทึกโดยผู้สอนfor left in range(TOTAL, -1, -1):
seg.text(str(left)) # ท่า widget: ทับค่าเดิม
arc.value(left)
lcd.clear() # ท่าลิ้นชัก: ล้างแล้ววาดใหม่ทั้งหน้า
lcd.console("<h2>นับถอยหลัง</h2>")
lcd.print("เหลืออีก", left, "วินาที")
ui.poll()
time.sleep_ms(TICK_MS)
ค่าที่เปลี่ยนตลอดเวลา ถ้าไล่พิมพ์ลงมาเรื่อย ๆ จอจะเต็มไปด้วยประวัติ แล้วคนดูต้องไล่หาเองว่าบรรทัดไหนคือค่าล่าสุด
ท่า widget ถูกกว่ามาก เพราะส่งข้ามคอร์ไปแค่ค่าใหม่ ไม่ได้ส่งทั้งหน้า
สร้าง widget ไว้นอกลูปเสมอ ถ้าย้ายสามบรรทัดนั้นเข้าไปในลูป จะได้ widget ใหม่ทุกวินาที
ห้ามสร้าง ui.Label ด้วยข้อความว่าง ระบบวาดจะเติมคำว่า Label ให้เอง แล้วคำนั้นค้างบนจอจนกว่าจะมีการเขียนทับครั้งแรก
ตาคุณ: ตั้ง TICK_MS = 150 แล้วขยาย TOTAL เป็น 20 รันดูแล้วตอบว่าท่าไหนอ่านออก ท่าไหนกลายเป็นจอกระพริบ
โปรแกรมที่ล้างจอเป็นสิ่งสุดท้ายก่อนจบ จะทิ้งจอว่างให้คนงงว่าเกิดอะไรขึ้น — ไฟล์นี้จึงปล่อยค่าสุดท้ายค้างไว้
examples/s01/06_safe_print.py↗ — ย้ายกฎเข้าไปอยู่ในฟังก์ชัน
examples/s01/06_safe_print.py↗ — บันทึกโดยผู้สอนfor ch in text:
size = len(ch.encode()) # ไทย 3 · อังกฤษ 1
# เช็ก "ก่อนใส่" ไม่ใช่ "หลังใส่"
if used + size > SAFE_BYTES:
flush(chunk)
chunk = ""
used = 0
chunk = chunk + ch
used = used + size
if chunk != "": # ก้อนสุดท้ายต้องส่งด้วย
flush(chunk)
รู้เพดาน 127 ไบต์แล้วก็จริง แต่ถ้าต้องนั่งนับไบต์ด้วยมือทุกบรรทัด สักวันจะลืมสักบรรทัด แล้วรายงานจะขาดครึ่งโดยไม่มี error ให้จับ
ความรู้ที่ต้องใช้วินัยของคนทุกครั้ง คือความรู้ที่จะพังในวันที่คนเหนื่อย ย้ายมันเข้าไปในฟังก์ชันเดียว แล้วโค้ดจำแทนให้
ไฟล์นี้เผื่อไว้ที่ SAFE_BYTES = 120 ต่ำกว่าเพดานจริง 127 อยู่ 7 ไบต์ เป็นระยะปลอดภัย
ข้อจำกัดที่ต้องรู้: ฟังก์ชันนี้ตัดที่ตัวอักษร ไม่ใช่ตัดที่คำ และมันไม่รู้จักแท็ก — ห้ามส่งข้อความที่มีแท็กเข้ามา เพราะแท็กอาจถูกตัดครึ่งกลางคัน
ตาคุณ: ลด SAFE_BYTES เหลือ 40 แล้วรันใหม่ จำนวนก้อนจะเพิ่มขึ้นแต่ตัวข้อความยังครบ ตอบว่าทำไมการแบ่งถี่ขึ้นถึงไม่ทำให้ตัวอักษรหายสักตัว
บรรทัด
if chunk != "":ท้ายฟังก์ชันคือบรรทัดที่คนลืมบ่อยที่สุด ลืมเมื่อไรท้ายประโยคหายทันที
examples/s01/07_ticks_and_beat.py↗ — ลูปที่สั่ง sleep เท่าเดิม ไม่ได้เดินตรงเวลาwork = time.ticks_diff(time.ticks_ms(), t_work) # งานรอบนี้กินไปกี่ ms
left = TARGET_MS - work # เหลือให้หลับเท่าไร
if left > 0:
time.sleep_ms(left)
sleep_ms() แปลว่า "หลับอย่างน้อยเท่านี้" ไม่ได้แปลว่า "รอบละเท่านี้" งานที่ทำก่อนหลับกินเวลาของมันเอง คาบจริงจึงยาวกว่าที่ขอเสมอ และส่วนเกินนั้น สะสมทุกรอบ
ห้ามลบเวลาสองค่าด้วยเครื่องหมายลบธรรมดา นาฬิกานี้นับขึ้นแล้ววนกลับ ticks_diff() รู้เรื่องการวน ส่วน t2 - t1 ไม่รู้ แล้วจะได้เลขติดลบมหาศาล
ui.Chart รับเฉพาะจำนวนเต็ม ช่วงแกนตั้งกำหนดตอนสร้างแล้วเปลี่ยนทีหลังไม่ได้ และมันเกิดมาพร้อมเส้นที่ 0 อยู่แล้ว add_series() จึงคืนเลข 1 เป็นเส้นแรกที่เราเพิ่ม — เก็บเลขที่มันคืนมาไว้ในตัวแปร อย่าเดาเอง
ตาคุณ: ตั้ง SWITCH_AT = 41 เพื่อปิดท่าที่ 2 ทิ้ง ทำนายก่อนรันว่าเลขช้าสะสมตอนจบจะออกมาราวเท่าไร แล้วรันเทียบ
นี่คือไฟล์แรกของชุดที่ไม่ได้แค่ให้ดู แต่ให้ ตั้งสมมติฐานแล้วรันพิสูจน์ — จดตัวเลขที่ทำนายไว้ก่อนกดรันเสมอ
examples/s01/08_status_screen.py↗ — จอบอกตอนนี้ ลิ้นชักบอกที่ผ่านมา # --- งานของจอ: ทำทุกรอบ ---
seg.text(str(value))
seg.color(color)
level_lbl.text(name)
level_lbl.color(color)
bar.value(value)
chart.set_next(s_value, value)
...
# --- งานของลิ้นชัก: ทำเฉพาะตอนมีเรื่องให้เล่า ---
if lv != last_level:
changes = changes + 1
last_level = lv
lcd.print("<span class=" + cls + ">" + str(elapsed) + " ms " +
name + " ค่า " + str(value) + "</span>")
ไฟล์นี้ไม่มีคำสั่งใหม่เลยสักตัว ทุกอย่างในนี้เคยผ่านตามาแล้วในไฟล์ 01 ถึง 07 ของใหม่คือ "จะเอามันมาต่อกันยังไง" ซึ่งเป็นคำถามที่ไฟล์เดี่ยว ๆ ไม่เคยตอบ
LEVELS เก็บชื่อ สี และคลาสของ span ไว้ด้วยกันในตารางเดียว จอกับลิ้นชักจึงเล่าเรื่องเดียวกันเสมอ เพราะทั้งคู่อ่านจากตารางนั้น
last_level = -1 แปลว่า "ยังไม่เคยรู้ระดับมาก่อน" รอบแรกจึงนับเป็นการเปลี่ยนเสมอ ถ้าตั้งต้นเป็น 0 ประวัติจะไม่มีบรรทัดแรกบอกว่าเริ่มที่ระดับไหน
ลูปในไฟล์นี้ใช้ท่าที่ 2 ของไฟล์ 07 ตรง ๆ — ของที่เรียนมาแล้วต้องกลับมาใช้ ไม่ใช่เรียนแล้วทิ้ง
มีงานวิจัยที่ไปสัมภาษณ์นักพัฒนามืออาชีพ 80 คนว่า ตัวอย่างโค้ดที่หาเจอบนเน็ตทำให้หงุดหงิดตรงไหนที่สุด คำตอบอันดับหนึ่งไม่ใช่ "โค้ดผิด" และไม่ใช่ "อธิบายน้อยไป" แต่คือ ตัวอย่างพวกนั้นไม่ช่วยให้คิดออกว่าจะเอาชิ้นส่วนมาต่อกันยังไง
ไฟล์ 01 ถึง 07 สอนชิ้นส่วนทีละชิ้น ซึ่งจำเป็นแต่ไม่พอ ไฟล์ 08 คือไฟล์เดียวในชุดที่ตอบคำถามว่าชิ้นส่วนพวกนั้นมาอยู่ในโปรแกรมเดียวกันได้ยังไง โดยไม่มีคำสั่งใหม่เข้ามาเลย
reading_at() ในไฟล์นั้นเป็นค่าอ่านจำลอง คาบ 5 เป็นต้นไปจะถอดฟังก์ชันนี้ทิ้งแล้วเสียบค่าจากเซนเซอร์จริงเข้ามาแทน ที่ยังจำลองไว้ก่อน เพราะคาบนี้เรากำลังเรียนเรื่องการรายงานผล ไม่ใช่การวัด
ตาคุณ: ย้ายบล็อก lcd.print() ออกจาก if ให้มันยิงทุกรอบ แล้วรันใหม่ เปิดลิ้นชักดู แล้วตอบว่าประวัติแบบไหนที่คนเดินมาดูหน้างานใช้งานได้จริงกว่ากัน (ใบ้: ลองหาคำตอบจากลิ้นชักว่า "ค่าขึ้นถึงระดับต้องรีบดูตอนวินาทีที่เท่าไร")
ถ้าจะเลือกอ่านซ้ำนอกเวลาแค่ไฟล์เดียวจากทั้งเก้าไฟล์ ให้เลือกไฟล์นี้
examples/s01/09_your_level_rule.py↗ — รันได้ แต่ยังตอบผิดทุกข้อCASES = (
(12, "ปกติ"),
(49, "ปกติ"),
(50, "เริ่มสูง"),
(79, "เริ่มสูง"),
(80, "ต้องรีบดู"),
(92, "ต้องรีบดู"),
)
...
# ----- เติมส่วนนี้เอง (งานของคุณ) -----
def level_name(v):
... # docstring ในไฟล์บอกโจทย์และใบ้วิธีเรียง if ไว้แล้ว
return "ปกติ" # ตอนนี้ตอบ "ปกติ" ทุกค่า จึงผ่านแค่สองข้อแรก
# ----- จบส่วนที่ต้องเติม -----
ไฟล์นี้เป็นคู่ฝึกของไฟล์ 08 ในนั้นกฎตัดระดับเขียนไว้ให้แล้ว ส่วนในนี้ยังว่าง และลอกจาก 08 มาตรง ๆ ไม่ได้ เพราะโจทย์คนละเจ้าใช้เส้นคนละที่
โจทย์มาจากทีมซ่อมบำรุง: ต่ำกว่า 50 คือปกติ · ตั้งแต่ 50 ถึง 79 ให้จับตาไว้ · ตั้งแต่ 80 ขึ้นไปต้องเข้าไปดูทันที นี่คือที่มาของตัวเลข ไม่ใช่เลขที่เราคิดขึ้นเอง
CASES เลือกไว้ให้มีค่าที่อยู่ ตรงเส้นพอดี (49, 50, 79, 80) เพราะนั่นคือจุดที่กฎผิดกันบ่อยที่สุด >= กับ > ต่างกันแค่ตัวเดียว
กับดักที่ต้องระวัง: ลำดับของ if สำคัญกว่าที่คิด ถ้าเช็กเงื่อนไข 50 ก่อน 80 ค่า 92 จะตกลงช่องกลางแล้วไม่มีวันไปถึงช่องบนเลย และไม่มี error ให้จับสักตัว
ตาคุณ (หลังผ่านครบหกข้อ): เพิ่มลงใน CASES อีกหนึ่งแถวที่คุณคิดว่ากฎของตัวเองน่าจะตก แล้วรันดูว่าตกจริงไหม ถ้าไม่ตก แปลว่ากฎแข็งกว่าที่คิด
ต้องทำในคาบ · เปิดตามลำดับนี้
| ลำดับ · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · 10 นาที | examples/s01/01_first_line.py↗ |
ตามหาข้อความให้เจอทั้งสามปลายทาง · จะรู้ว่าจอเงียบมักแปลว่าเรายืนผิดหน้า ไม่ใช่โค้ดพัง |
| 2 · 10 นาที | examples/s01/02_markup_tags.py↗ |
เทียบรายงานสามชุดในลิ้นชัก · จะเข้าใจว่าสีคือระดับความสำคัญ ไม่ใช่ของตกแต่ง |
| 3 · 15 นาที | examples/s01/03_byte_limit.py↗ |
พิมพ์ชื่อไทยเข้าไปแล้วดูว่าตัวเลขไบต์เปลี่ยนเป็นแดงตอนไหน · จะรู้ว่าจอนับไบต์ ไม่ได้นับตัวอักษร |
| 4 · 15 นาที | examples/s01/08_status_screen.py↗ |
ประกอบสามโมดูลเป็นจอเดียว · ไฟล์ที่ควรอ่านซ้ำที่สุดในชุด |
| 5 · 20 นาที | examples/s01/09_your_level_rule.py↗ |
เขียนกฎเอง แล้วให้บอร์ดตรวจจนหกแถวเขียวหมด |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| ข้อความของทีมขาดหายท้ายบรรทัด ทั้งที่ไม่มี error ให้จับสักตัว | examples/s01/06_safe_print.py↗ |
| รายงานยาวจนคนดูตอบไม่ได้ว่าตกลงผ่านหรือไม่ผ่าน | examples/s01/04_console_drawer.py↗ |
| ค่าที่เปลี่ยนทุกวินาทีไหลลงจนเต็มจอ อ่านไม่ทัน | examples/s01/05_clear_and_refresh.py↗ |
| ลูปเดินช้ากว่าที่สั่งไว้ และยิ่งนานยิ่งเพี้ยน | examples/s01/07_ticks_and_beat.py↗ |
ทุกไฟล์จบด้วยบล็อก ตาคุณ — อ่านแล้วรันแล้วยังไม่จบ ต้องแก้แล้วรันซ้ำถึงจะจบ
เก้าไฟล์ที่ผ่านมาใช้ lcd time ui สามตัว ซึ่งเป็นแค่สามในสิบเอ็ดโมดูลที่บอร์ดนี้มี
หกไฟล์ต่อไปนี้ไม่ได้ให้พิมพ์ตาม — ให้รันแล้วดู ไฟล์ละไม่เกินห้านาที เป้าหมายคือจบคาบนี้แล้วรู้ว่าอีกสิบเอ็ดคาบข้างหน้าจะได้เล่นอะไรบ้าง ไม่ใช่รู้ทุกฟังก์ชัน
| ไฟล์ | เปิดโมดูล | ชื่อที่จะได้เห็นทำงานจริง |
|---|---|---|
examples/s01/10_board_knows_itself.py↗ |
gpio · machine |
gpio.board_info() gpio.led() gpio.button() — และพิสูจน์ว่า machine.PWM กับ machine.ADC ไม่มีอยู่จริงในพอร์ตนี้ |
examples/s01/11_lights_and_a_button.py↗ |
gpio |
gpio.num_leds() gpio.num_buttons() · เมธอดของหลอด on() off() toggle() |
examples/s01/12_every_sense_at_once.py↗ |
sensors |
sensors.snapshot() คืนทุกเซนเซอร์ในครั้งเดียว (ทั้งสองบอร์ด) · และ init() scan() ที่ Eva Kit ปฏิเสธ พร้อมบอกเหตุผล ส่วน Dev Kit ยอมให้เรียก — ไฟล์พิมพ์คำตอบของบอร์ดตรงหน้า ไม่ได้เดาให้ |
examples/s01/13_raw_and_filtered.py↗ |
dsp |
dsp.EMA dsp.Median dsp.tilt — ค่าดิบกับค่ากรอง วาดทับกันบนกราฟเดียว |
examples/s01/14_the_board_hears_you.py↗ |
mic |
mic.start() mic.level() mic.stats() mic.lag() — และหน้าจอ DotMatrix |
examples/s01/15_one_number_many_faces.py↗ |
ui เต็มรูปแบบ |
เลขตัวเดียวขับ เก้า widget พร้อมกัน — Arc Bar Chart Seg7 Slider Switch Checkbox Spinner Compass · บวก ui.sfx() ui.tone() |
ครูเปิดทีละไฟล์บนจอหน้าห้อง ห้องรันตาม แล้วถามคำถามเดียวต่อไฟล์ — "ของนี้เอาไปทำอะไรได้ในงานของคุณ" คำตอบที่ได้คือวัตถุดิบของโปรเจกต์จบในคาบ 12
ไฟล์
examples/s01/12_every_sense_at_once.py↗ กับexamples/s01/15_one_number_many_faces.py↗ คือสองไฟล์ที่ต้องได้เล่นแน่ ๆ ถ้าเวลาไม่พอ — ตัวแรกแสดงว่าบอร์ดรับรู้โลกได้กี่ทาง ตัวหลังแสดงว่าเลขหนึ่งตัวเล่าเรื่องได้กี่แบบ
เข้าใจเส้นทางนี้แล้ว การดีบักจะเลิกเป็นการเดา — และเข้าใจด้วยว่าทำไม
ui.poll()ถึงจำเป็น มันคือจังหวะที่เราเปิดกล่องจดหมายให้อีกฝั่งหยิบของไป
ในชิปตัวเดียวกันมีซีพียูสองตัวที่ทำงานคนละแบบ
ใครอ่านเซนเซอร์ตัวไหน ต่างกันระหว่างสองบอร์ด: บน Eva Kit CM55 เป็นเจ้าของบัสเซนเซอร์ทั้งหมด (IMU แผ่นสัมผัส ลูกบิด) Python จึงต้องขอค่าผ่าน IPC และ sensors.init() ถูกปฏิเสธ · บน Dev Kit CM33 อ่าน IMU/เข็มทิศเองได้ (sensors.init() ทำงาน) ส่วนแผ่นสัมผัสกับลูกบิดยังอ่านผ่าน CM55 เหมือนกัน — sensors.snapshot() ซ่อนความต่างนี้ให้ คืน dict รูปเดียวกันทั้งสองบอร์ด
โค้ด Python ของเราอยู่ฝั่ง CM33 เสมอ อยากให้อะไรขึ้นจอ ต้อง ฝากข้อความข้ามไปให้ CM55 วาด
IoT = อุปกรณ์มีเซนเซอร์ + ต่อเน็ต ส่งข้อมูลขึ้นระบบกลาง
AIoT = ย้าย การตัดสินใจ ลงมาไว้ที่ตัวอุปกรณ์เอง ไม่ต้องรอถามคลาวด์ทุกครั้ง
ตัวอย่างที่จับต้องได้: เครื่องจักรตัวหนึ่งสั่นผิดปกติ

เทอร์โมสตัทสัมผัสในบ้านจริงตัวนี้ทำครบวงในกล่องเดียว — วัดอุณหภูมิ ตัดสินว่าร้อนไปหรือเย็นไป แล้วสั่งคอมเพรสเซอร์ทำงาน ลองชี้ให้ได้ว่าสามหน้าที่นั้นอยู่ตรงไหนของกล่องนี้
และนี่คือรูปร่างของไฟล์ที่ 8 กับ 9 ที่เพิ่งรันไปเป๊ะ ๆ — วัดค่า ตัดระดับ แล้วรายงาน
โรงงาน — Predictive Maintenance
มอเตอร์ปั๊มน้ำมีเซนเซอร์ความสั่นติดอยู่ ระบบบนตัวมันรู้ว่า "เสียงสั่นปกติ" หน้าตาแบบไหน พอลูกปืนเริ่มสึก รูปแบบการสั่นเปลี่ยนก่อนพัง 2-3 สัปดาห์ ระบบแจ้งซ่อมล่วงหน้า แทนที่จะรอสายพานหยุดกลางกะ
อาคาร — Smart Building
เซนเซอร์ในห้องประชุมนับคนจากความเคลื่อนไหวและเสียง แล้วสั่งแอร์ให้แรงตามจำนวนคนจริง ไม่ใช่ตั้งไว้ 22 องศาทั้งวัน ค่าไฟลดได้ 20-30% โดยไม่มีใครต้องกดสวิตช์
สุขภาพ — Fall Detection
อุปกรณ์ติดตัวผู้สูงอายุ แยกให้ออกระหว่าง "นั่งลงเร็ว" กับ "ล้ม" — สองอย่างนี้กราฟความเร่งคล้ายกันมาก ค่าเดียวที่จุดเดียวแยกไม่ออก ต้องดูรูปร่างของสัญญาณทั้งช่วง



ทั้งสามเคสนี้ ตัวเซนเซอร์กับตัวตัดสินใจอยู่ที่เดียวกัน — นั่นแหละ AIoT

| ของบนบอร์ด | มันวัด/ทำอะไร | เจอในเมนูไหน · ใช้เองคาบไหน |
|---|---|---|
| BMI270 | ความเร่ง 3 แกน + การหมุน 3 แกน (ทั้งสองบอร์ด) | Home, Dashboard, Smart Watch · เราใช้เองคาบ 6-7 |
| BMM350 | สนามแม่เหล็กโลก → ทิศเหนือ (ทั้งสองบอร์ด) | Dashboard (เข็มทิศ) · คาบ 8 |
| CapSense | แผ่นสัมผัส — Eva: ปุ่มสัมผัส 2 ปุ่ม + แถบเลื่อน (5 อิเล็กโทรด อ่านเป็นค่าเดียว 0–100) · Dev Kit: ดูตำแหน่งที่บอร์ดของทีม | Home (Eva: Controls ด้วย) · คาบ 5 |
| Potentiometer | ลูกบิดหมุน → ค่าอนาล็อก — Eva: ลูกบิดสีน้ำเงิน 1 ตัว · Dev Kit: VR1–VR4 (sensors.pot อ่าน VR1) |
Home (Eva: Controls ด้วย) · คาบ 5 |
| LED + ปุ่มผู้ใช้ | LED — Eva 3 ดวง · Dev Kit 5 ดวง (ถาม gpio.num_leds() อย่าจำเลข) · ปุ่มที่ Python ใช้ได้ 1 ปุ่ม เรียกด้วยชื่อจาก gpio.button(0).name() ไม่ใช่ป้ายบนแผ่นวงจร |
Eva: Controls · คาบ 3 |
| ไมโครโฟน PDM | เสียงรอบตัว | ยังไม่มีเมนูของตัวเอง · คาบ 8 ใช้ผ่านโมดูล mic |
| WiFi/BT (Eva: CYW55513IUBG) | Wi-Fi + Bluetooth ใช้วิทยุร่วมกัน (Eva: Wi-Fi 6 2.4/5 GHz + Bluetooth 5.4) | Wi-Fi Setting · คาบหน้าเราต่อเอง |
| จอสัมผัส 4.3" | หน้าต่างของทุกอย่าง — พื้นที่วาด 792×398 เท่ากันทั้งสองบอร์ด | ทุกเมนู · วันนี้เลย |
| เฉพาะ Dev Kit | SHT40 (อุณหภูมิ/ความชื้น) · DPS368 (ความกดอากาศ) · เรดาร์ · RGB dot matrix · CAN · ปุ่มเสริมสองปุ่ม (import buttons) |
Home (แถว Temp / Humid) · คาบ 9–12 ใช้ SHT40 เมื่อบอร์ดมี |
จำตารางนี้ไว้ — คอลัมน์ขวาคือแผนที่ของทั้งเทอม เราจะไล่หยิบของในตารางนี้มาสั่งงานเองทีละตัว · ตัวเลขที่ต่างกันระหว่างสองบอร์ด (จำนวน LED, จำนวนลูกบิด) ให้ถามบอร์ดด้วยโค้ดเสมอ ไม่ต้องจำ
สี่อย่างนี้เป็นงานของผู้สอน ไม่ใช่ของผู้เรียน ตรวจให้ครบก่อนเริ่ม แล้วคาบจะเดินได้ตลอดสามชั่วโมงโดยไม่สะดุด · ข้อที่สามตรวจเหมือนกันทั้งสองบอร์ด: แตะแผ่นสัมผัสแล้วแถว Touch บนหน้า Home ต้องเปลี่ยน — ถ้าไม่เปลี่ยน สาเหตุที่ต้องสงสัยก่อนคือชิป CapSense ยังไม่ถูก flash ซึ่งเกิดได้กับทั้งสองบอร์ด เพราะแผ่นสัมผัสของทั้งคู่ต่อกับชิป PSoC 4000T แยกต่างหาก ที่ต้องโปรแกรมคนละรอบกับตัวบอร์ดหลัก · ข้อที่สี่มีเฉพาะ Eva Kit เพราะ Dev Kit ไม่มีการ์ด Audio Player
ข้อที่พลาดบ่อยที่สุดคือชิป CapSense — มันเป็นชิปแยกที่ต้องโปรแกรมต่างหากจากตัวบอร์ดหลัก ทั้งสองบอร์ดเหมือนกัน
ทีมสาธิตการใช้ 5 เมนูพร้อมอธิบายว่าเมนูใดใช้เซนเซอร์ใด + รัน lcd.print() ข้อความของทีมขึ้นจอสำเร็จ
แปลเป็นสิ่งที่ตรวจได้จริง:
<h2> ของคาบนี้examples/s01/09_your_level_rule.py↗ ผ่านครบหกแถว (เขียวหมด)ไม่มีข้อไหนต้องใช้โค้ดเกินสามสิบบรรทัด — คาบนี้วัดความเข้าใจ ไม่ใช่ปริมาณ
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| อยู่หน้า Playground แล้ว แต่พื้นที่แสดงผลว่าง มีจุดแดงเล็ก ๆ ที่ปุ่มไอคอนมุมขวาล่าง | หน้านี้เปิดมาที่โหมด UI ข้อความ lcd ถูกพักไว้ |
แตะปุ่มไอคอนสีเขียวมุมขวาล่าง เพื่อสลับเป็นโหมดข้อความ |
สร้าง ui.Label แล้วจอนิ่งไปราวสองวินาที |
ยังไม่ได้เคาะ ui.poll() |
เติม ui.poll() หนึ่งครั้งหลังสร้าง/แก้ widget |
ป้ายบนจอขึ้นคำว่า Label ที่เราไม่ได้พิมพ์ |
สร้าง ui.Label ด้วยข้อความว่าง |
ตั้งข้อความเริ่มต้นให้มันเสมอ |
| ส่งโค้ดแล้วเงียบสนิท ไม่มีแม้แต่จุดแดง | โค้ดยังไม่ถึงบอร์ด หรือสคริปต์ error ก่อนถึงบรรทัด lcd |
ดู error ที่คอนโซล IDE แล้วส่งใหม่ |
| ข้อความไปโผล่ที่คอนโซลคอมแทน | ใช้ print() แทน lcd.print() |
เปลี่ยนเป็น lcd.print() |
| ตัวอักษรโตผิดปกติเฉพาะบรรทัดนั้น | ลืมปิดแท็ก </h2> |
ตรวจว่าแท็กเปิด-ปิดครบคู่ |
| ข้อความยาวถูกตัดหาย แถวถัดไปต่อท้ายมาเลย | เกิน 127 ไบต์/ครั้ง (ไทย ≈ 42 ตัวอักษร) — ตัดแล้ว \n หายด้วย |
แบ่งเป็นหลาย lcd.print() หรือใช้ท่าของไฟล์ 06 |
เรียก seg.value(12) แล้วได้ 12 ไม่ใช่ 12.0 |
.value() ส่งได้แต่จำนวนเต็ม |
ใช้ seg.text("12.0") |
| หน้า Audio Player ว่างเปล่า (Eva Kit) | บอร์ดไม่ได้เสียบ SD card | ไม่ใช่ความผิดพลาด ข้ามไปเมนูอื่น |
| หาการ์ด Controls / Audio Player / TESAIoT Connect ไม่เจอ | ถือ Dev Kit อยู่ — build ของคอร์สไม่มีสามการ์ดนี้ | ไม่ใช่ความผิดพลาด เกณฑ์ผ่านใช้เฉพาะเมนูที่มีทั้งสองบอร์ด |
| แตะการ์ดแล้วจอค้างแวบหนึ่ง | บางหน้าใช้เวลาสร้างครั้งแรก | รอสองวินาที อย่ารัวแตะซ้ำ |
เกือบทุกข้อในตารางนี้ ไม่ใช่ความผิดของโค้ด แต่เป็นความไม่รู้ลำดับขั้นตอน
เปิด practise_codes/s01_hello_lcd.py↗ มีช่องว่างให้เติม 7 จุด (ท่าที่ 4 แยกเป็นสองบรรทัดเพราะเรื่อง 127 ไบต์)
# เติม: lcd.clear()
pass
# เติม: lcd.console("<h2>AIoT in Action - คาบ 1</h2>")
pass
for i in range(len(MEMBERS)):
# เติม: lcd.print("สมาชิกคนที่", i + 1, ":", MEMBERS[i])
pass
# เติม: time.sleep_ms(800)
pass
ทำสามขั้น: หนึ่ง แก้ข้อมูลทีมสามบรรทัดบนสุด สอง เติมทีละจุดแล้วส่งขึ้นบอร์ดทุกครั้ง สาม ครบแล้วลองเพิ่มบรรทัดของตัวเอง
อย่าเติมครบเจ็ดจุดแล้วค่อยรันทีเดียว — เติมทีละจุดแล้วรัน จะรู้ทันทีว่าจุดไหนพัง
s01_hello_lcd.py↗ — ส่วนที่หนึ่งอ่านให้เข้าใจ แล้วพิมพ์เอง อย่าคัดลอกวาง การพิมพ์เองคือตอนที่มือกับสมองจำโครงสร้างได้
# s01_hello_lcd.py - ข้อความแรกของทีม ขึ้นจอบอร์ด
import lcd
import time
import ui
TEAM_NAME = "BentoBuilders"
MEMBERS = ["สมชาย", "สมหญิง", "สมศรี"]
MOTTO = "เล่นของจริงก่อน แล้วค่อยแกะ"
import lcd ต้องมาก่อนใช้งานเสมอ ส่วน import time เอาไว้ใช้ sleep_ms() ในท่าที่ 3 และ import ui เอาไว้วางป้ายลงหน้าจอในท่าที่ 5
การประกาศข้อมูลทีมไว้บนสุดแยกจากโค้ดข้างล่าง ทำให้คนที่มาอ่านทีหลังแก้ข้อมูลได้โดยไม่ต้องเข้าใจตรรกะทั้งไฟล์
การวางค่าที่ต้องแก้บ่อยไว้บนสุด เป็นมารยาทที่ดีต่อคนอ่านคนถัดไป (ซึ่งมักคือตัวเราเองในอีกสองสัปดาห์)
lcd.clear()
lcd.console("<h2>AIoT in Action - คาบ 1</h2>")
lcd.console("<span class=muted>ทดสอบจอครั้งแรกของทีม " + TEAM_NAME + "</span>")
lcd.print("สวัสดีจากทีม", TEAM_NAME)
lcd.print("คำขวัญของเรา:", MOTTO)
print("ส่งข้อความทักทายขึ้นจอบอร์ดแล้ว ดูที่จอ ไม่ใช่ที่หน้าคอม")
สังเกตความต่างสองแบบของการประกอบข้อความ: บรรทัด <span class=muted> ใช้ + เพราะต้องแทรกชื่อทีม ไว้กลางแท็ก ส่วน lcd.print("สวัสดีจากทีม", TEAM_NAME) ใช้จุลภาคเพราะไม่มีแท็กมาเกี่ยว
ถ้าใช้จุลภาคกับแท็ก จะได้ <span class=muted> ทีม </span> ที่มีช่องว่างเกินติดขอบแท็ก
ทำไมต้อง lcd.clear() ก่อนเสมอ — ลิ้นชักอาจมีข้อความจากโปรแกรมของกลุ่มก่อนหน้าค้างอยู่ การเริ่มจากสถานะที่เรารู้แน่นอน เป็นนิสัยของงาน embedded จริง ไม่ใช่แค่ความเรียบร้อย
เลือกวิธีต่อสตริงตามว่า "มีแท็กมาเกี่ยวไหม" ไม่ใช่ตามความเคยชิน
for i in range(len(MEMBERS)):
lcd.print("สมาชิกคนที่", i + 1, ":", MEMBERS[i])
time.sleep_ms(800)
TAIL = "ทีม " + TEAM_NAME + " พร้อมลุยคาบต่อไป"
lcd.print("<span class=ok>ขึ้นจอสำเร็จ</span>")
lcd.print("<span class=ok>" + TAIL + "</span>")
range(len(MEMBERS)) ให้ตัวเลข 0, 1, 2 ตามจำนวนสมาชิกจริง — ถ้าทีมมีสองคน ลูปจะวนสองรอบเอง ไม่ต้องแก้อะไร ข้อมูลเป็นตัวขับการแสดงผล
การหน่วง 800 ms อยู่ ในลูป ไม่ใช่นอกลูป ถ้าย้ายออกไปข้างนอก ชื่อทุกคนจะขึ้นพร้อมกันแล้วค่อยหน่วงครั้งเดียว — ลองย้ายดูแล้วสังเกตความต่าง จะเข้าใจเรื่อง indent ในภาษา Python ทันที
บรรทัดปิดท้ายแยกเป็นสอง lcd.print() ตั้งแต่ต้น เพราะรวมกันแล้วเสี่ยงเกิน 127 ไบต์ถ้าชื่อทีมยาว ประโยคปิดท้ายถูกยกออกมาเก็บไว้ในตัวแปร TAIL เพราะท่าที่ 5 ต้องเอาความยาวของมันไปวัด
ใน Python การเยื้องบรรทัดคือความหมาย ไม่ใช่แค่ความสวยงาม
lcd ทำแทนไม่ได้ui.screen()
time.sleep_ms(200)
COL_TEXT, COL_DIM = 0xE8EAED, 0x9AA3AF
COL_CARD, COL_OK, COL_RUN = 0x171B22, 0x30A46C, 0x4A9EFF
BYTE_LIMIT = 127
ui.Panel(x=16, y=8, w=760, h=372, color=COL_CARD, min=COL_DIM, max=12, value=1)
ui.Label("ป้ายประจำทีม", x=32, y=16, color=COL_DIM, value=20)
ui.Label(TEAM_NAME, x=32, y=40, color=COL_TEXT, value=28)
ui.Label(MOTTO, x=32, y=84, color=COL_DIM, value=20)
tbl = ui.Table(x=32, y=116, w=340, h=252, cols=2)
tbl.col_width(0, 100)
tbl.col_width(1, 230)
tbl.add_row("ลำดับ", "ชื่อสมาชิก")
ui.Label("สถานะการเขียนตาราง", x=400, y=116, color=COL_DIM, value=20)
led_run = ui.Led(x=408, y=144, w=48, h=48, color=COL_RUN, value=1)
ui.Label("กำลังเขียน", x=452, y=148, color=COL_DIM, value=20)
led_done = ui.Led(x=408, y=188, w=48, h=48, color=COL_OK, value=0)
ui.Label("เขียนครบแล้ว", x=452, y=192, color=COL_DIM, value=20)
for i in range(len(MEMBERS)):
tbl.add_row(str(i + 1), MEMBERS[i])
ui.poll()
time.sleep_ms(500)
led_run.value(0)
led_done.value(1)
ลูปนี้เดินรายชื่อ ชุดเดียวกัน กับท่าที่ 3 แต่ปลายทางคนละที่ — ท่าที่ 3 พิมพ์เข้าลิ้นชัก ท่านี้เขียนลงตารางบนจอ ข้อมูลชุดเดียวไปได้สองที่พร้อมกัน และนั่นคือสิ่งที่คาบนี้อยากให้เห็น · ui.Table จัดคอลัมน์ให้เอง ถ้าเรียง ui.Label เองต้องนับพิกเซลทุกแถว พอชื่อยาวไม่เท่ากันคอลัมน์ที่สองจะเยื้องจนอ่านไม่ออก แถวหนึ่งสูงราว 62 พิกเซล หัวตารางบวกสมาชิกสามคนจึงต้องการ h=252
ไฟสองดวงบอกสถานะแทนการเปลี่ยนสีตัวอักษร เหตุผลอยู่ที่ การทดสอบขาวดำ — ถ่ายรูปจอแล้วแปลงเป็นเกรย์สเกล ไฟที่ติดกับไฟที่หรี่ยังแยกออกด้วยความสว่าง ส่วนตัวหนังสือสีเขียวกับสีเทากลายเป็นสีเดียวกัน · led_run.value(0) ทำให้ไฟ หรี่ ไม่ใช่หาย โดยตั้งใจ ไฟแผงควบคุมที่หายไปตอนดับ ทำให้คนดูแยกไม่ออกว่าดับจริงหรือจอเสีย
used = len(TAIL.encode())
ui.Label("ความยาวบรรทัดปิดท้าย จากเพดาน 127", x=400, y=240, color=COL_DIM, value=20)
ui.Bar(x=408, y=272, w=332, h=16, color=COL_OK, min=0, max=BYTE_LIMIT, value=used)
ui.Scale(x=408, y=288, w=332, h=44, color=COL_TEXT, min=0, max=BYTE_LIMIT)
ui.Label(str(used) + " ไบต์", x=408, y=336, color=COL_TEXT, value=20)
ui.poll()
เลข 72 ลอย ๆ ไม่บอกอะไรเลย เลข 72 ที่มีไม้บรรทัด 0–127 อยู่ใต้มันบอกทันทีว่าเหลือที่อีกเกินครึ่ง นี่คือกฎเดียวกับที่หน้าจอโรงงานใช้ — ค่าที่วัดได้ต้องมาพร้อมพิสัยหรือเกณฑ์ของมัน
ui.Scale ไม่รับ .value() มันคือไม้บรรทัด ตัวที่ขยับคือ ui.Bar ที่เราวางทับไว้ข้างบน (แบบวงกลมมีเข็มจริงผ่าน prop — คาบ 6 สอน) ลองเรียก .value(50) ใส่ Scale ดูก็ได้ แล้วจะเห็นว่าไม่มีอะไรเกิดขึ้น
ภาษาไทยตัวละ 3 ไบต์ บรรทัดที่ดูสั้นบนจอคอมจึงกินโควตาเร็วกว่าที่ตาประเมิน — มาตรวัดนี้ทำให้เรื่องนั้นมองเห็นได้แทนที่จะต้องจำ
ท่า 1 ล้างจอ + หัวเรื่อง มาก่อน เพราะต้องพิสูจน์ให้ได้ก่อนว่า "ช่องทางสื่อสารกับจอใช้ได้จริง" ถ้าท่านี้ไม่ขึ้น ท่าที่เหลือไม่มีประโยชน์ที่จะเขียนต่อ
ท่า 2 ข้อความคงที่ มาก่อนลูป เพราะการพิมพ์ค่าตายตัวง่ายกว่า และแยกได้ว่าปัญหาอยู่ที่ lcd หรืออยู่ที่ตรรกะของเรา
ท่า 3 ลูป มาหลังจากพิสูจน์สองข้อบนแล้ว ตอนนี้ถ้าพัง เรารู้แน่ว่าพังที่ลูป ไม่ใช่ที่จอ
ท่า 4 สถานะปิดท้าย มาก่อนป้าย เพราะมันคือ "สัญญาณว่าทุกอย่างข้างบนผ่านหมดแล้ว" โปรแกรมที่ดีต้องบอกให้รู้ว่า มันจบแล้ว และจบแบบสำเร็จ ไม่ใช่เงียบหายไปเฉย ๆ
ท่า 5 ป้ายบนหน้าจอ มาสุดท้าย เพราะมันคือของที่เหลือไว้ให้คนอื่นอ่าน หลังโปรแกรมจบและหลังลิ้นชักถูกปิดไปแล้ว สี่ท่าแรกคุยกับคนที่นั่งดูตอนรัน ท่านี้คุยกับคนที่เดินผ่านโต๊ะทีหลัง
นี่คือวิธีคิดแบบ ไล่จากง่ายไปยาก แล้วให้แต่ละขั้นยืนยันขั้นก่อนหน้า ซึ่งเป็นวิธีดีบักงาน embedded มาตรฐาน
ถ้าเขียนรวดเดียวสี่สิบบรรทัดแล้วรัน พอมันเงียบ น้อง ๆ จะไม่รู้เลยว่าต้องเริ่มหาจากตรงไหน
ฝั่งระบบสมองกลฝังตัว
สถาปัตยกรรมหลายคอร์และการแบ่งงานตามความถนัด · การสื่อสารระหว่างคอร์ผ่าน IPC · แนวคิดว่าเฟิร์มแวร์คือชั้นที่ซ่อนความยุ่งยากของฮาร์ดแวร์ไว้ให้เรา
ฝั่ง Python และวิทยาการคอมพิวเตอร์
import โมดูล · ตัวแปร list และ tuple · ลูป for กับ range() · การเขียนฟังก์ชันด้วย def · การเข้ารหัสข้อความเป็นไบต์ · นาฬิกาที่วนกลับกับ ticks_diff()
ฝั่งการออกแบบระบบ
การเริ่มจากสถานะที่รู้แน่ · การรายงานสถานะเมื่อจบงาน · การแยกข้อมูลออกจากตรรกะ · การแบ่งหน้าที่ว่าจอตอบ "ตอนนี้" ลิ้นชักตอบ "ที่ผ่านมา"
สามบรรทัดสุดท้ายคือของที่จะติดตัวไปใช้ได้แม้เปลี่ยนภาษาและเปลี่ยนบอร์ด
วันนี้เราได้:
เล่นบอร์ดครบทุกเมนูหลักและรู้ว่าแต่ละเมนูกินข้อมูลจากเซนเซอร์ตัวไหน · รู้จักสมองสองก้อนและเส้นทางที่ข้อความเดินทางไปถึงจอ · ใช้ lcd ui และ time ประกอบเป็นจอสถานะหนึ่งใบได้ · เขียนกฎตัดระดับเองแล้วให้บอร์ดตรวจจนผ่าน
การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ "ต่อยอด" บันทึกลง worksheet ข้อ 8
คาบหน้า: เราจะพาบอร์ดออกอินเทอร์เน็ต ด้วยโค้ดของเราเอง ไม่ใช่กดผ่านเมนู — wifi.connect() ให้ค่าอะไรกลับมา wifi.ip() คืออะไร แล้วทำไม "ต่อติดแล้ว" กับ "ยังต่ออยู่" ถึงเป็นคนละคำถาม · แล้วเลขที่วันนี้จบอยู่บนจอบอร์ด จะออกไปโผล่บนหน้าเว็บ (สไลด์ถัดไป)
เก็บโค้ดของวันนี้ไว้ให้ดี คาบต่อ ๆ ไปเราจะต่อยอดจากไฟล์เดิมเรื่อย ๆ — โดยเฉพาะโครงของไฟล์ 08
วันนี้ตัวเลขทุกตัวจบที่จอบอร์ด examples/s01/12_every_sense_at_once.py↗ อ่านลูกบิด แผ่นสัมผัส และ IMU ได้ในคำสั่งเดียว แต่คนที่เห็นมีแค่คนที่ยืนอยู่หน้าบอร์ด
คาบหน้าเราเอาเลขชุดเดียวกันนี้ส่งออกไปที่ broker สาธารณะ broker.hivemq.com แล้วเปิดหน้าเว็บ examples/web/my_first_reader.html บนโน้ตบุ๊กของเราเองอ่านกลับมา ใช้แค่เบราว์เซอร์ ไม่ต้องติดตั้งอะไรเพิ่ม
ของที่ติดมือไปคาบหน้า: sensors.snapshot() จากไฟล์ 12 คือแหล่งตัวเลข · โครงจอสถานะของไฟล์ 08 คือหน้าตาของบันได WiFi → broker → ส่ง ที่ต้องเห็นบนจอทีละขั้น · ชื่อทีม team01 ถึง team19 ที่ผู้สอนแจก ให้จดลงใบงาน
Emulator ใน ide.tesaiot.dev มีโมดูล
mqttที่ต่อ broker สาธารณะตัวจริงผ่าน WebSocket ได้ เลขจาก Emulator จึงขึ้นหน้าเว็บได้เหมือนเลขจากบอร์ด
คำถามคิดต่อ: เมนูไหนที่เล่นวันนี้ ที่อยากสร้างเองมากที่สุด · ถ้าจะสร้างมัน ต้องรู้อะไรเพิ่มบ้าง · ข้อมูลจากเซนเซอร์ตัวไหนที่น่าจะมีประโยชน์กับงานที่ทีมทำอยู่จริง
| คาบ | เรื่อง | จบคาบแล้วทำอะไรได้ |
|---|---|---|
| 3 | ควบคุมฮาร์ดแวร์ — LED ปุ่ม จอ | สั่งไฟติดดับ อ่านปุ่มโดยไม่โดนสัญญาณเด้งหลอก |
| 4 | สร้าง Touch UI คุมฮาร์ดแวร์ | แตะปุ่มบนจอแล้วไฟบนบอร์ดติดจริง |
| 5 | อนาล็อก + สัมผัส + กรองสัญญาณ | หมุนลูกบิดคุมค่า และทำให้เลขที่สั่นนิ่งลงได้ |
| 6 | IMU กับมุมเอียง | ทำเครื่องวัดระดับดิจิทัลที่เอียงตามบอร์ดจริง |
| 7 | กราฟเรียลไทม์หลายเส้น | วาดสัญญาณที่วิ่งอยู่ และรู้ว่าสุ่มช้าไปแล้วภาพหลอกยังไง |
| 8 | Mini-HMI ประกอบทุกอย่าง | หน้าจอเดียวที่รวมทุกเซนเซอร์ และไม่ตายเมื่อตัวใดตัวหนึ่งเงียบ |
| 9 | WiFi และเครือข่าย | จอสถานะเครือข่ายที่บอกได้ว่าหลุดตอนไหน และกลับมาเมื่อไร |
| 10 | MQTT — ส่งค่าและรับคำสั่ง | ค่าจากโต๊ะเราขึ้นจอคนอื่น และคำสั่งจากคนอื่นสั่งบอร์ดเราได้ |
| 11 | MQTTs เข้ารหัส สู่แพลตฟอร์ม | ส่งข้อมูลแบบที่คนกลางดักอ่านไม่ได้ ขึ้นแพลตฟอร์มจริง |
| 12 | Capstone — สร้างของจริงของทีม | ผลิตภัณฑ์ AIoT ย่อมหนึ่งชิ้นที่ทีมออกแบบเอง ตั้งแต่เซนเซอร์ถึงหน้าจอถึงคลาวด์ |
เส้นเรื่องคือเส้นเดียว — คาบ 3 ถึง 8 ทำให้บอร์ดรับรู้และแสดงผลได้ครบ · คาบ 9 ถึง 11 พาออกไปหาโลก · คาบ 12 คือวันที่ทีมเอาทุกชิ้นมาประกอบเป็นของตัวเอง
รายละเอียดเต็ม พร้อมภาพหน้าจอของแต่ละคาบ อยู่ที่
slides/roadmap.html— เปิดดูล่วงหน้าได้เลย
ทั้งหมดนี้เป็นการบ้านแบบสมัครใจ ไม่ต้องเปิดในคาบ เนื้อหาวันนี้เข้าใจได้ครบโดยไม่ต้องดู
ไมโครโฟน MEMS ดิจิทัลหน้าตาเป็นอย่างไร
InvenSense (TDK) · 2 นาที 16 วินาที · อังกฤษ
บอร์ดเรามีไมโครโฟนแบบนี้ (Eva Kit มีสองตัว · Dev Kit ดูที่บอร์ดของทีม) คลิปนี้เปิดให้เห็นว่าข้างในมีอะไรและมันส่งอะไรออกมา — คาบ 8 เราจะอ่านค่าจากมันด้วยโมดูล mic
ข้างในเซนเซอร์วัดการเคลื่อนไหวมีอะไรอยู่จริง ๆ
Breaking Taps · ความยาว: ยังไม่ยืนยัน · อังกฤษ
แกะฝาชิป IMU แล้วส่องด้วยกล้องจุลทรรศน์อิเล็กตรอน เห็นมวลกับซี่หวีที่ขยับได้จริง — ตอบข้อสงสัยที่เกือบทุกคนมีว่า "มันวัดการเอียงได้ยังไงในเมื่อไม่มีอะไรหมุน"
BMI270 ตัวเดียวกับที่อยู่บนบอร์ดเรา
SparkFun Electronics · ความยาว: ยังไม่ยืนยัน · อังกฤษ
เบอร์ชิปตรงกับ U5 ในตารางเมื่อครู่เป๊ะ ๆ คลิปนี้ต่อสายจริงแล้วอ่านค่าออกมา — เห็นว่าสิ่งที่แถว IMU บนหน้า Home แสดงอยู่ มาจากชิ้นส่วนที่ซื้อแยกได้ ไม่ใช่ของวิเศษเฉพาะบอร์ดนี้
อ่านต่อสำหรับคนอยากรู้ลึก
คลิปพวกนี้ไม่ได้อยู่ในเกณฑ์ผ่าน แต่คนที่ดูจะเข้าใจคาบหลัง ๆ ได้เร็วกว่าเพื่อน
ข้อ 1 · จอต้อนรับของทีม
ทำหน้าจอต้อนรับที่มีหัวเรื่อง ชื่อทีม คำขวัญ และรายชื่อสมาชิกพร้อม "หน้าที่ในทีม" ของแต่ละคน ใช้ระดับสีให้ตรงความหมาย ไม่ใช่เลือกตามชอบ
ข้อ 2 · นับถอยหลัง
ดัดแปลง examples/s01/05_clear_and_refresh.py↗ ให้นับถอยหลังจากเวลาที่ทีมตั้งเอง แล้วจบด้วยข้อความสีเขียว ลองปรับ TICK_MS แล้วสังเกตความรู้สึกที่ต่างกัน
ข้อ 3 · สำรวจกับดัก 127 ไบต์
จงใจพิมพ์ข้อความไทยยาวเกิน 42 ตัวอักษรในครั้งเดียว วัดดูว่าตัดที่ตัวอักษรที่เท่าไร แล้วอธิบายว่าทำไมภาษาไทยกับภาษาอังกฤษได้จำนวนตัวอักษรไม่เท่ากัน
ข้อ 4 · แผนที่เมนู
ทำตารางในสมุดว่าเมนูทั้งหมดที่เล่นวันนี้ ใช้เซนเซอร์อะไร แสดงผลแบบไหน และถ้าเป็นทีมเรา จะเพิ่มเมนูที่หกเป็นอะไรเพื่อแก้ปัญหาในงานจริงของเรา
เขียนคำตอบลง worksheet ข้อ 8 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า




.name() คือ "USER Button 1" รอถ่ายใหม่) · 12 คำสั่งเดียว ได้ทุกเซนเซอร์พร้อมกัน

เอกสารของผู้ผลิต
งานวิจัยที่กำหนดรูปร่างของคาบนี้
examples/s01/08_status_screen.py↗ ได้พื้นที่สองสไลด์หมายเหตุเรื่องภาพ
ไดอะแกรม SVG ทุกภาพในเด็คนี้วาดขึ้นใหม่สำหรับหลักสูตรนี้ โดยอ้างอิงตำแหน่งอุปกรณ์และหมายเลขขาจากคู่มือบอร์ดข้างต้น
ภาพถ่ายและภาพเคลื่อนไหวที่นำมาประกอบ มาจาก Wikimedia Commons และหน่วยงานรัฐ ภายใต้สัญญาอนุญาต CC0 · CC BY · CC BY-SA หรือสาธารณสมบัติ ระบุผู้สร้างและสัญญาอนุญาตไว้ใต้ภาพทุกภาพ
ภาพหน้าจอในเด็คนี้มาจากสามแหล่ง และคำบรรยายใต้ภาพระบุไว้ทุกใบว่าใบไหนมาจากไหน — ภาพถ่ายจากบอร์ด Eva Kit จริง บันทึกโดยผู้สอน · ภาพจาก BENTO Emulator ที่ 800×480 เท่าจอของทั้งสองบอร์ด · และ ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด แต่ค่าจากเซนเซอร์ WiFi และไมโครโฟนเป็นค่าแทนบนเครื่องโฮสต์ — ภาพจากตัวจำลองแสดงหน้าจอที่ตัวอย่างสร้าง ไม่ใช่ผลการวัดของบอร์ด
ข้อเท็จจริงเกี่ยวกับพฤติกรรมของ lcd ui เมนู และเฟิร์มแวร์ ตรวจสอบจากซอร์สโค้ดของโปรเจกต์ KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw (Eva Kit) TESAIoT_KIT_PSE84_AI-Micropython-BentoClaw (Dev Kit) และ BENTO-TESAIoT-libraries โดยตรง
ทุกตัวเลขบนสไลด์นี้สืบกลับไปที่เอกสารต้นทางหรือซอร์สโค้ดได้ ถ้าเจอที่ไม่ตรง บอกผู้สอนได้เลย
fit-css
VIDEO-SLOT: คลิป 60-90 วินาที ถ่ายจอบอร์ดจริง (ถ่ายทั้ง Eva Kit และ Dev Kit ถ้าทำได้) — ไล่จาก Home ที่ค่าเซนเซอร์วิ่ง → (เฉพาะ Eva Kit: แตะ Controls แล้วแตะวงกลมให้ LED บนบอร์ดติด — Dev Kit ไม่มีการ์ดนี้ ให้รัน examples/s01/11 แทน) → Sensor Dashboard กราฟเลื่อน → Smart Watch ปัดเปลี่ยนหน้า → ปิดท้ายที่ BENTO Playground ที่ยังว่างเปล่า พร้อมคำบรรยายว่าหน้านี้คือหน้าที่โค้ดของเราจะมาโผล่
ไฟล์นี้ไม่มีเฉลยแยกให้ เพราะจอบนบอร์ดคือเฉลย — แก้แล้วรันใหม่ได้เรื่อย ๆ จนหกแถวเขียวหมด
หัวใจของ AIoT คือ ตัดสินใจใกล้จุดเกิดเหตุ ส่งขึ้นคลาวด์เฉพาะสิ่งที่มีความหมาย
VIDEO-SLOT: คลิป 20-30 วินาที ถ่ายจอบอร์ดตอนผ่าน MVP — หัวเรื่องขึ้น ชื่อสมาชิกไล่ทีละคน แล้วปิดด้วยบรรทัดสีเขียวสองบรรทัด ใช้เป็นตัวอย่าง "หน้าตาของคำว่าผ่าน" ให้ทุกทีมเทียบ
สีทั้งห้าตัวบนสุดคือจานสีของหลักสูตร (SPEC §S7.13) บทบาทละหนึ่งค่า — ห้ามหยิบสีสถานะมาแต่งจอ
ผู้สอน: ท่าที่ 5 นี้ในไฟล์ฝึกเขียนมาให้ครบแล้ว ไม่มีช่องว่าง ให้ทีมอ่านให้จบก่อนรัน แล้วเทียบว่าของที่ ui วางกับของที่ lcd พิมพ์ไปคนละที่กันอย่างไร