สามตัวนี้คือของที่จะพิมพ์เองครบทุกบรรทัดในครึ่งแรก ส่วนครึ่งหลังของชุดบทเรียนจะเปิดอีกห้าโมดูล — 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 รอบทเรียน 2.4–2.6) · บรรทัดพวกนี้ตัดมาจาก 01_first_line.py ตรง ๆ — COL_ACCENT คือสีเน้น 0x4A9EFF ที่หัวไฟล์ประกาศไว้
value= บน ui.Label คือขนาดตัวอักษร ไม่ใช่ตัวเลขที่จะเอาไปแสดง — จุดนี้ทำคนสะดุดทุกรุ่น

lcd ครั้งแรก แต่เปิดรอไว้จะเห็นผลตั้งแต่วินาทีแรก)examples/ ของบทเรียน 1.1–1.3 แล้วกด Program to Devicelcd จะรออยู่จนกว่าจะกดสลับ สังเกตจุดแดงเล็ก ๆ ที่มุมปุ่มเมื่อมีข้อความรอพื้นที่วาดของเราคือ 792 x 398 พิกเซล และมุมขวาล่างราว 100x58 เป็นของปุ่ม Console ที่เฟิร์มแวร์จองไว้ — วาง widget ทับตรงนั้นแล้วจะกดเปิดลิ้นชักไม่ได้
ทีมละหนึ่งบอร์ด — สลับกันเป็นคนพิมพ์ทุกช่วง อย่าให้ใครนั่งดูอย่างเดียวทั้งบทเรียน
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(...) — แก้ข้อความบนป้ายเดิม ไม่ใช่วางป้ายใหม่ทับ ป้ายหนึ่งใบที่เปลี่ยนค่าได้ อ่านง่ายกว่าป้ายสิบใบที่กองทับกัน
ตาคุณ (ท้ายไฟล์): เพิ่มชื่อทีมเข้าไปในทั้งสามปลายทาง แล้วตอบว่าต้องเปิดดูที่ไหนบ้างถึงจะเห็นครบทั้งสามที่
จอเงียบมักแปลว่าเรายืนผิดหน้า ไม่ใช่โค้ดพัง — ไฟล์นี้มีไว้พิสูจน์ประโยคนั้นด้วยตาตัวเอง
02_markup_tags.py — สีคือระดับ ไม่ใช่ของตกแต่งfor cls, text in REPORT:
# แท็กเปิดกับแท็กปิดต้องอยู่ในการเรียกครั้งเดียวกันเสมอ
lcd.print("<span class=" + cls + ">" + text + "</span>")
ห้าคลาสนี้คือ ห้าระดับความสำคัญ ไม่ใช่ห้าสีให้เลือกตามชอบ ชุดที่ 3 ในไฟล์จงใจทาข่าวดีเป็นสีแดง เพื่อให้เห็นว่าสีผิดทำร้ายคนอ่านก่อนที่เขาจะทันได้อ่านตัวหนังสือ
ฝั่งจอแกะแท็กทีละบรรทัด จบบรรทัดแล้วสถานะเริ่มใหม่หมด ลืมปิด </h2> ผลกระทบจำกัดอยู่ในบรรทัดนั้นบรรทัดเดียว
กับดัก: พิมพ์ชื่อคลาสผิด เช่น class=okay ไม่มี error ให้จับสักตัว บรรทัดนั้นแค่ออกมาเป็นสีปกติ — เจอบรรทัดที่ไม่มีสี ให้สงสัยชื่อคลาสก่อน
ตาคุณ: ใน REPORT มีบรรทัดหนึ่งติดระดับไว้ผิด หาให้เจอแล้วแก้ จากนั้นเพิ่มข่าวของทีมเองอีกหนึ่งบรรทัดพร้อมเลือกระดับให้มัน
เกณฑ์ตัดสินง่าย ๆ: บรรทัดนี้ทำให้คนที่เดินผ่านต้องลุกจากเก้าอี้ไหม ถ้าไม่ ก็ไม่ใช่
error
03_byte_limit.py — เพดาน 127 ไบต์
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 อีกหนึ่งแถว ทำนายก่อนรัน ว่าจะได้กี่ไบต์และจะเป็นเขียวหรือแดง แล้วรันเทียบกับที่ทำนาย
การตัดที่ต้องตัด ให้ตัดที่ ตัวอักษร ไม่ใช่ที่ไบต์ — ตัดกลางไบต์ของตัวอักษรหนึ่งตัวแล้วจอจะแสดงเป็นขยะ
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 พิกเซลหรือเปล่า)
โปรแกรมที่พูดเฉพาะตอนสำเร็จ จะเงียบสนิทตอนล้มเหลว ซึ่งคือจังหวะที่คนใช้อยากรู้ที่สุด
05_clear_and_refresh.py — เขียนทับที่เดิม
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 รันดูแล้วตอบว่าท่าไหนอ่านออก ท่าไหนกลายเป็นจอกระพริบ
โปรแกรมที่ล้างจอเป็นสิ่งสุดท้ายก่อนจบ จะทิ้งจอว่างให้คนงงว่าเกิดอะไรขึ้น — ไฟล์นี้จึงปล่อยค่าสุดท้ายค้างไว้
06_safe_print.py — ย้ายกฎเข้าไปอยู่ในฟังก์ชัน
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 != "":ท้ายฟังก์ชันคือบรรทัดที่คนลืมบ่อยที่สุด ลืมเมื่อไรท้ายประโยคหายทันที
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 ทิ้ง ทำนายก่อนรันว่าเลขช้าสะสมตอนจบจะออกมาราวเท่าไร แล้วรันเทียบ
นี่คือไฟล์แรกของชุดที่ไม่ได้แค่ให้ดู แต่ให้ ตั้งสมมติฐานแล้วรันพิสูจน์ — จดตัวเลขที่ทำนายไว้ก่อนกดรันเสมอ
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() ในไฟล์นั้นเป็นค่าอ่านจำลอง บทเรียน 2.7–2.9 เป็นต้นไปจะถอดฟังก์ชันนี้ทิ้งแล้วเสียบค่าจากเซนเซอร์จริงเข้ามาแทน ที่ยังจำลองไว้ก่อน เพราะชุดบทเรียนนี้เรากำลังเรียนเรื่องการรายงานผล ไม่ใช่การวัด
ตาคุณ: ย้ายบล็อก lcd.print() ออกจาก if ให้มันยิงทุกรอบ แล้วรันใหม่ เปิดลิ้นชักดู แล้วตอบว่าประวัติแบบไหนที่คนเดินมาดูหน้างานใช้งานได้จริงกว่ากัน (ใบ้: ลองหาคำตอบจากลิ้นชักว่า "ค่าขึ้นถึงระดับต้องรีบดูตอนวินาทีที่เท่าไร")
ถ้าจะเลือกอ่านซ้ำนอกเวลาแค่ไฟล์เดียวจากทั้งเก้าไฟล์ ให้เลือกไฟล์นี้
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 นาที | 01_first_line.py |
ตามหาข้อความให้เจอทั้งสามปลายทาง · จะรู้ว่าจอเงียบมักแปลว่าเรายืนผิดหน้า ไม่ใช่โค้ดพัง |
| 2 · 10 นาที | 02_markup_tags.py |
เทียบรายงานสามชุดในลิ้นชัก · จะเข้าใจว่าสีคือระดับความสำคัญ ไม่ใช่ของตกแต่ง |
| 3 · 15 นาที | 03_byte_limit.py |
พิมพ์ชื่อไทยเข้าไปแล้วดูว่าตัวเลขไบต์เปลี่ยนเป็นแดงตอนไหน · จะรู้ว่าจอนับไบต์ ไม่ได้นับตัวอักษร |
| 4 · 15 นาที | 08_status_screen.py |
ประกอบสามโมดูลเป็นจอเดียว · ไฟล์ที่ควรอ่านซ้ำที่สุดในชุด |
| 5 · 20 นาที | 09_your_level_rule.py |
เขียนกฎเอง แล้วให้บอร์ดตรวจจนหกแถวเขียวหมด |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| ข้อความของทีมขาดหายท้ายบรรทัด ทั้งที่ไม่มี error ให้จับสักตัว | 06_safe_print.py |
| รายงานยาวจนคนดูตอบไม่ได้ว่าตกลงผ่านหรือไม่ผ่าน | 04_console_drawer.py |
| ค่าที่เปลี่ยนทุกวินาทีไหลลงจนเต็มจอ อ่านไม่ทัน | 05_clear_and_refresh.py |
| ลูปเดินช้ากว่าที่สั่งไว้ และยิ่งนานยิ่งเพี้ยน | 07_ticks_and_beat.py |
ทุกไฟล์จบด้วยบล็อก ตาคุณ — อ่านแล้วรันแล้วยังไม่จบ ต้องแก้แล้วรันซ้ำถึงจะจบ
ไฟล์นี้ไม่มีเฉลยแยกให้ เพราะจอบนบอร์ดคือเฉลย — แก้แล้วรันใหม่ได้เรื่อย ๆ จนหกแถวเขียวหมด