| คำถาม | demo ตอบว่า | product ต้องตอบว่า |
|---|---|---|
| ทำไมต้องมีของชิ้นนี้ | เพราะมันเท่ | เพราะมีคนเสียเงินหรือเสียเวลากับปัญหานี้อยู่ |
| ค่าที่เห็นบนจอคืออะไร | ตัวเลขจากเซนเซอร์ | ปริมาณที่มีหน่วย และมีคนตัดสินใจจากมันได้ |
| ส่งอะไรขึ้นไป | ทุกอย่างที่วัดได้ | เฉพาะสิ่งที่ปลายทางใช้จริง |
| เน็ตหลุดแล้วยังไง | ค้าง หรือ error กลางจอ | จอทำงานต่อ บอกสถานะ แล้วต่อเองเมื่อเน็ตกลับมา |
| ใครดูแลมันต่อ | ไม่มีใคร | มี device id แยกตัว มีสถานะให้ตรวจ |
ช่องขวาทั้งห้าข้อ ไม่มีข้อไหนต้องใช้ API ใหม่ที่เรายังไม่เคยเรียนเลยสักตัว
ระยะทางจาก demo ถึง product สั้นกว่าที่คิด แต่ต้องตั้งใจเดิน ไม่ใช่บังเอิญไปถึง

| คำถาม | คำตอบของคาบนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| Why | ในเมื่อไม่มี API ใหม่สักตัว ทำไมยังต้องมีคาบนี้ | เพราะระยะทางจาก demo ถึง product ไม่ได้วัดด้วยจำนวนบรรทัด มันวัดด้วย การตัดสินใจ — เลือกโจทย์จากความเจ็บปวดจริง ออกแบบข้อมูลให้ฝั่งรับใช้ต่อได้ และตัดสินไว้ล่วงหน้าว่าตอนพังจะให้เกิดอะไร · ของที่ไม่เคยถูกทดสอบตอนเน็ตหลุด ยังไม่ใช่ของที่ส่งมอบได้ | ครึ่งแรก · demo กับ product · canvas · ออกแบบตอนพัง |
| What | มีอะไรให้ใช้บ้าง | ทุกโมดูลที่คอร์สนี้เปิดไปแล้ว — เก้าโมดูล ตั้งแต่ lcd ถึง tesaiot และวันนี้ ไม่มีชื่อใหม่เพิ่มแม้แต่ชื่อเดียว |
สไลด์บัญชีรวมเก้าโมดูล |
| How | ประกอบยังไงให้ใช้งานได้จริง | กรอก canvas ห้าช่อง Sense → Decide → Show → Send → Act ให้ครบก่อนแตะโค้ด แล้วเติมโครง s12_capstone_starter.py ที่จุด "ทีมเขียนเอง" ห้าจุด |
หกไฟล์ตัวอย่าง + โครงตั้งต้น + นำเสนอ 10 นาที |
ปลายทางที่จับต้องได้ — ของหนึ่งชิ้นที่วางหน้าห้องแล้วเดินครบวงเอง ตั้งแต่วัด ตัดสิน แสดง ถึงรายงานขึ้น broker และทีมอธิบายได้ว่ามันแก้ปัญหาอะไรให้ใคร รวมทั้งบอกได้ว่ามันยังทำอะไรไม่ได้
คาบ 11 เราทำให้ส่งอย่างปลอดภัยได้ · คาบนี้เราตอบคำถามที่มาก่อนหน้านั้น — จะส่งอะไร ให้ใคร และเขาจะเอาไปทำอะไรต่อ
s12_capstone_starter.py จนรัน end-to-end แล้ว นำเสนอ 10 นาทีปลายทางของวันนี้: บอร์ดวางอยู่หน้าห้อง จอบอกสถานะ ข้อความไหลขึ้น broker และทีมอธิบายได้ว่ามันแก้ปัญหาอะไรให้ใคร
วันนี้เราไม่ได้เรียน API ใหม่ เราเรียน วิธีตัดสินใจ ว่าจะเอา API ที่มีอยู่ไปทำอะไร

team01-eva ในภาพคือ device_id รุ่นก่อนเปลี่ยนชื่อ ปัจจุบันไฟล์ใช้ team01โครงของหน้าจอรุ่นปัจจุบัน แบ่งเป็นสามการ์ด
ui.Bar วางทับ ui.Scale — ค่ากับพิสัยอยู่ด้วยกัน และมีป้ายบอกคุณภาพของค่าเองว่าสดหรือค้างนี่คือ starter ไม่ใช่เฉลย — สิ่งที่ให้มาคือ มาตรฐานของหน้าจอ ที่ทีมต้องรักษาไว้ ส่วนเนื้อในเปลี่ยนเป็นโจทย์ของทีมได้ทั้งหมด

| คาบ | สิ่งที่ได้ | วันนี้เอามาใช้ตรงไหน |
|---|---|---|
| 1 | ทัวร์เมนู · lcd.print() · แนวคิด "ส่งเฉพาะสิ่งที่มีความหมาย" |
ช่อง Show และ Send ของ canvas |
| 2 | wifi.connect() · เลข IP ของบอร์ด · กฎว่าลิงก์แบบไหนเรียกว่าใช้ได้ |
ช่อง Send — ต้องรู้ว่าสายดีจริงก่อนจะเชื่อว่าส่งถึง |
| 3 | gpio · ลูป polling · จังหวะเวลา |
โครงลูปหลักของโปรแกรม |
| 4 | ui.Button/Switch/Label · ui.poll() · กฎเหล็ก 5 ข้อ |
ปุ่มรับทราบและหน้าจอสถานะ |
| 5 | sensors.pot · capsense · dsp.EMA |
การกรองสัญญาณก่อนตัดสิน |
| 6 | bmi270.motion() · dsp.tilt() |
ค่าตัวอย่างในโครงเริ่มต้น |
| 7 | ui.Chart หลาย series · cadence 200 ms |
กราฟย้อนหลังบนหน้าจอผลงาน |
| 8 | Panel การ์ด · งบ 64 widgets · เกณฑ์สองระดับกันป้ายกระพริบ · รันยาว 10 นาที | โครงหน้าจอของผลิตภัณฑ์ และช่อง Decide ที่ป้ายไม่สั่น |
| 9 | wifi.connect/is_connected/ip/ping |
การตรวจสายก่อนส่ง |
| 10 | mqtt.connect/publish/subscribe · topic · JSON |
ช่อง Send |
| 11 | tesaiot.* · serverTLS 8884 · ตัวตนอุปกรณ์ |
ทางยกระดับความปลอดภัยของผลงาน |
ไม่มีอะไรในตารางนี้ที่ทีมยังไม่เคยรันเอง วันนี้แค่เอามาต่อกัน
| โมดูล | มีกี่ชื่อ | เปิดที่คาบไหน | งานจบใช้ตรงช่องไหนของ canvas |
|---|---|---|---|
lcd |
4 — print console clear theme |
คาบ 1 | Show — ทางที่ง่ายที่สุดตอนยังไม่มีหน้า HMI |
gpio |
18 — ระดับโมดูล 7 · เมธอดของ LED 8 · ของ Button 3 |
คาบ 3 | Act — ทางเดียวในคอร์สนี้ที่สั่งของจริงให้ขยับ |
ui |
52 บนโมดูล · บวกเมธอดของ Widget อีก 14 |
คาบ 4 · 7 · 8 | Show — และเป็นทางที่คนสั่งงานเข้ามาด้วย |
sensors |
10 ชื่อระดับโมดูล (Eva: ห้าตอบ ห้าปฏิเสธ · Dev Kit: init() scan() ทำงานได้ แต่กติกาของคอร์สคือไม่ต้องเรียก) · บวกกลุ่มเซนเซอร์ที่บอร์ดมี — Eva สี่กลุ่ม (bmi270 bmm350 capsense pot) · Dev Kit เพิ่ม sht40 dps368 เรดาร์ |
คาบ 5 · 6 · 8 | Sense |
dsp |
16 — 8 คลาส 8 ฟังก์ชัน (สเปกตรัมสองตัวเพิ่ม 2026-08-20) | คาบ 5 · 6 · 7 | ระหว่าง Sense กับ Decide — กรองก่อนตัดสิน |
mic |
8 — โมดูล Python ที่ฝังมากับเฟิร์มแวร์ | คาบ 8 | Sense ทางเลือก สำหรับโจทย์ที่วัดด้วยเสียง |
wifi |
8 | คาบ 9 | ตรวจสายให้แน่ก่อนจะเชื่อว่าส่งถึง |
mqtt |
6 | คาบ 10 | Send |
tesaiot |
28 — ใช้ได้จริง 9 ทั้งสองบอร์ด · สิบหกตัวข้ามคอร์ไปหาชิปที่ไม่ได้เปิด (ได้ OSError · เวลาที่เสียยังไม่ได้วัด) · protected_update() ห้ามเรียก (บน Dev Kit เขียนลงชิปจริง) |
คาบ 11 | Send แบบยกระดับ ผ่านพอร์ต 8884 |
อย่านับซ้ำ — "ตระกูล IMU สิบสี่ชื่อ" ของคาบ 6 ไม่ใช่โมดูลที่สิบ มันคือ sensors.bmi270 ห้า + sensors.bmm350 ห้า + dsp อีกสี่ ที่ซ้อนอยู่ในตารางแล้ว
ของที่คอร์สนี้ไม่เคยมีให้ — ไม่มี machine.PWM .ADC .SPI .Timer · dsp มี fft_mag (ตั้งแต่ 2026-08-20) แต่ไม่มี ulab · Eva Kit ไม่มี DPS368 SHT40 เรดาร์ ส่วน Dev Kit มีทั้งสาม (sensors.sht40 อุณหภูมิ/ความชื้น · sensors.dps368 ความกดอากาศ · sensors.radar()) แต่คอร์สไม่ได้เปิดสอน — ทีมบน Dev Kit ใช้ได้ถ้าอ่านซอร์สเอง และต้องมีแผนสำรองบน Eva เสมอ · optiga เป็นการสาธิตท้ายคาบ 11 ไม่อยู่ในเกณฑ์ อย่าวางแผนงานจบทับมัน
ถ้าโจทย์ที่ทีมเลือกต้องการชื่อที่ไม่มีในตารางนี้ ให้รู้ตั้งแต่วันนี้ ไม่ใช่รู้ตอนเหลือเวลาชั่วโมงเดียว — ทางออกคือหา ตัวแทนที่ประกาศตัวว่าเป็นตัวแทน ตามสไลด์ "เมื่อบอร์ดวัดสิ่งที่เราอยากรู้ไม่ได้"
วิธีที่คนส่วนใหญ่เริ่ม แล้วจบไม่สวย: "บอร์ดมี IMU กับเข็มทิศ เราจะทำอะไรกับมันดี"
วิธีที่ได้ของที่มีคนใช้: เริ่มจากสามคำถามนี้ตามลำดับ
พอตอบครบสามข้อ ค่อยถามว่า "บอร์ดของเราวัดอะไรที่พอจะบอกเรื่องนั้นได้บ้าง"
ทีมที่เดินลำดับนี้จะได้โจทย์ที่เล่าให้คนนอกฟังได้ใน 30 วินาที ทีมที่เดินย้อนกลับมักจบด้วยของสวยที่ไม่มีใครอยากได้
โจทย์ที่ดีเล่าจบก่อนที่คนฟังจะทันถามว่า "แล้วไง"

canvas นี้อยู่ในใบงานข้อ 4.1 กรอกให้ครบก่อนแตะคีย์บอร์ด
โจทย์ตัวอย่าง: นั่งร้านชั้นสามของไซต์ก่อสร้าง เอียงผิดปกติแล้วไม่มีใครรู้จนเช้า
| ช่อง | คำตอบของทีมตัวอย่าง |
|---|---|
| Sense | sensors.bmi270.motion() → dsp.tilt() ทุก 200 ms กรองด้วย dsp.EMA(alpha=0.2) วัดเทียบท่าตั้งต้นตอนติดตั้ง |
| Decide | เกิน 8° = เฝ้าดู, เกิน 15° = ผิดปกติ ต้องเกินติดกัน 3 รอบจึงเชื่อ ตัดสินบนบอร์ดทั้งหมด |
| Show | ตัวเลของศาตัวใหญ่ + แท่งระดับ + คำว่า OK/WARN/ALERT + สถานะเน็ต + ปุ่มรับทราบ |
| Send | JSON 6 ฟิลด์ ไป bento/team01/telemetry — ส่งตอนสถานะเปลี่ยน + heartbeat ทุก 30 วินาที |
| Act | หัวหน้าไซต์ได้แจ้งเตือน เดินไปดูภายใน 15 นาที ถ้าเป็นของจริงต่อเข้าไลน์กลุ่มหรือ SMS |
สังเกตว่าช่อง Act เขียนเป็น "คนทำอะไร ภายในกี่นาที" ไม่ใช่ "ส่งขึ้นคลาวด์" — ถ้าปลายทางไม่มีใครทำอะไร ข้อมูลนั้นไม่ต้องส่งตั้งแต่แรก
ตัวอย่างนี้คือโจทย์ที่เฉลย
solution_codes/s12_capstone_starter.py↗ ทำจนจบ
| โจทย์ | วัดด้วยอะไรบนบอร์ดนี้ | เกณฑ์ที่ใช้ได้จริง | ข้อจำกัดที่ต้องพูดตรง ๆ |
|---|---|---|---|
| 1 · เฝ้าความสั่นมอเตอร์ปั๊ม | bmi270.motion() แล้วคิดขนาดความเร่งรวม |
ค่าเฉลี่ยเคลื่อนที่สูงกว่าค่าปกติ 2 เท่า ติดกัน 5 รอบ | บอร์ดวัดความสั่นระดับหยาบ ไม่ใช่ระดับ FFT ของเครื่องมือวัดจริง |
| 2 · ห้องเย็นอุณหภูมิหลุดเกณฑ์ | บน Eva Kit ไม่มีเซนเซอร์อุณหภูมิห้อง → ใช้ตัวแทน: ประตูถูกเปิดค้าง (การเอียงของบานประตู) · บน Dev Kit อ่านตรงได้ จาก sensors.sht40.temperature() |
ประตูขยับแล้วไม่กลับที่เดิมใน 60 วินาที (Eva) · อุณหภูมิเกินเกณฑ์ติดกัน N รอบ (Dev Kit) | ตัวแทนบอกได้แค่ "สาเหตุ" ไม่ได้บอก "อุณหภูมิ" ต้องเขียนไว้ในข้อจำกัด · ทีมที่ใช้ SHT40 ต้องบอกว่างานนี้ย้ายไป Eva ไม่ได้โดยไม่เปลี่ยน Sense |
| 3 · ติดตามการเคลื่อนย้ายทรัพย์สิน | bmi270.motion() + bmm350.heading() |
ทิศเปลี่ยนเกิน 30° หรือมีความเร่งเกินเกณฑ์ = ของถูกยก | จับได้ว่า "ขยับ" แต่บอกไม่ได้ว่า "ไปอยู่ที่ไหน" (ไม่มี GPS) |
| 4 · นับเวลาเครื่องจักรเดินเบา | ความสั่นต่ำกว่าเกณฑ์ต่อเนื่อง = idle | สะสมนาที idle ต่อกะ ส่งสรุปทุก 15 นาที | ต้องปรับเกณฑ์กับเครื่องจริงก่อน ค่าจากห้องเรียนใช้ไม่ได้ตรง ๆ |
| 5 · เฝ้าการใช้งานห้องประชุม | การเคลื่อนไหวจาก IMU + capsense เป็นปุ่มเช็กอิน |
มีคนแตะเช็กอิน = ใช้งานอยู่, ไม่มีการขยับ 10 นาที = ห้องว่าง | ตัวแทนที่หยาบ ของจริงใช้ PIR หรือกล้องนับคน |
| 6 · เตือนการเอียงของชั้นวาง/นั่งร้าน | dsp.tilt() เทียบท่าตั้งต้น |
เกิน 15° ติดกัน 3 รอบ | ต้องยึดบอร์ดให้แน่นจริง ไม่งั้นวัดการเอียงของเทปกาว |
เลือกโจทย์ที่ทีมมีคนเคยเจอปัญหานั้นจริง จะเถียงกันเรื่องเกณฑ์ได้สนุกกว่ามาก

หมายเหตุเรื่องเสียง: ไมโครโฟนใช้จาก Python ได้แล้ว โมดูล mic ฝังมากับเฟิร์มแวร์ทั้งสองบอร์ด (boards/KIT_PSE84_EVAL_EPC2/manifest.py และ boards/KIT_PSE84_AI/manifest.py freeze ไฟล์เดียวกัน) และในอีมูเลเตอร์ — mic.start() แล้ว mic.level() คืนความดัง 0-100 ส่วน mic.rms() กับ mic.peak() คืนค่าดิบ 0-32768 · วัดจริงบน Eva Kit 14 ส.ค. 2026 (ตัวเลขบน Dev Kit ยังไม่ได้วัด) ห้องเงียบได้ rms 30 (ระดับ 7) เปิดโทนใส่ไมค์ได้ 19335 (ระดับ 92) ต่างกัน 645 เท่า ซึ่งห่างพอจะตั้งเกณฑ์ตัดสินได้จริง · level() นับเป็นอ็อกเทฟแบบที่หูได้ยิน ไม่ใช่สัดส่วนตรงของสเกลเต็ม จึงขยับตั้งแต่เสียงพูดปกติ
โจทย์ที่ต้องฟังเสียงเริ่มได้เลยวันนี้ — เกณฑ์พลังงานจาก peak() พอสำหรับโจทย์ระดับคาบนี้แล้ว แล้วครอบด้วยกฎ confirm-N เหมือนค่าจากเซนเซอร์ตัวอื่นทุกประการ
ตัวแทนที่ตอบได้ 1 บิต ก็ยังต้องผ่านกฎ confirm-N — สัญญาณเด้งของหน้าสัมผัสคือ "ค่าสั่นวูบเดียว" ในอีกหน้าตาหนึ่ง

Eva Kit มี IMU เข็มทิศ ปุ่มสัมผัส ลูกบิด และจอ — ไม่มีเซนเซอร์อุณหภูมิห้อง ความชื้น ก๊าซ หรือกระแสไฟ · TESAIoT Dev Kit มีชุดเดียวกัน และเพิ่ม SHT40 (อุณหภูมิ/ความชื้น) DPS368 (ความกดอากาศ) กับเรดาร์ — แต่ก็ยังไม่มีก๊าซ ไม่มีกระแสไฟ เรื่อง proxy จึงเป็นเรื่องของทั้งสองบอร์ด ต่างกันแค่ว่าอะไรบ้างที่ต้องใช้ตัวแทน
sensors.bmi270.temperature() มีชื่ออยู่ในโมดูลก็จริง แต่บน Eva Kit มัน โยน OSError ทุกครั้ง เพราะค่านี้ไม่ได้อยู่ใน snapshot ของคอร์จอ และถึงอ่านได้ (บน Dev Kit อ่านได้) มันคืออุณหภูมิของชิป IMU ซึ่งอุ่นตามการทำงานของบอร์ด ไม่ใช่ของห้อง — อุณหภูมิห้องบน Dev Kit ต้องมาจาก sensors.sht40
ทางออกที่วิศวกรใช้จริงคือ proxy — วัดสิ่งที่วัดได้ ซึ่งสัมพันธ์กับสิ่งที่อยากรู้: ห้องเย็นอุ่นขึ้นไหม → ประตูเปิดค้างหรือเปล่า · เครื่องทำงานอยู่ไหม → ความสั่นของโครง · มีคนอยู่ในห้องไหม → การเคลื่อนไหวกับการแตะปุ่ม
กติกาข้อเดียวของการใช้ proxy: บอกให้ชัดว่ามันคือตัวแทน ทั้งบนสไลด์นำเสนอและในชื่อฟิลด์ของ schema — ตั้งชื่อ door_open_s ไม่ใช่ temp_c
proxy ที่ประกาศตัวว่าเป็น proxy คืองานวิศวกรรม · proxy ที่แอบอ้างเป็นของจริงคือการหลอกลูกค้า
payload ของเราในโครงเริ่มต้นหน้าตาแบบนี้
{"id": "team01", "v": 17.4, "unit": "deg", "state": "ALERT", "kind": "event", "t": 812340}
หกฟิลด์ และทุกฟิลด์มีเหตุผล
| ฟิลด์ | ทำไมต้องมี |
|---|---|
id |
ปลายทางต้องแยกออกว่าข้อความนี้มาจากบอร์ดตัวไหน — บน broker สาธารณะยิ่งจำเป็น |
v |
ค่าที่ตัดสินใจแล้ว ไม่ใช่ค่าดิบสามแกน ปัดทศนิยมให้พอใช้ ไม่ต้องส่ง 6 ตำแหน่ง |
unit |
ตัวเลขที่ไม่มีหน่วยคือตัวเลขที่ตีความผิดได้ ยานอวกาศเคยตกเพราะเรื่องนี้ |
state |
คำตัดสินของบอร์ด ปลายทางไม่ต้องมาคำนวณซ้ำและได้คำตอบไม่ตรงกัน |
kind |
บอกว่านี่คือ event, heartbeat หรือการรับทราบ — คนละความหมาย คนละการกระทำ |
t |
เวลาบนบอร์ด ใช้เรียงลำดับและดูว่าข้อความมาช้าไปแค่ไหน |
กติกาที่ทำให้ schema อยู่ได้นาน: ชื่อฟิลด์คงที่ตลอดโครงการ · เพิ่มฟิลด์ใหม่ได้ แต่ห้ามเปลี่ยนความหมายของฟิลด์เดิม · payload ขาออกกระชับ ต่ำกว่า 1000 ไบต์เสมอ
ฝั่งรับเขียนโค้ดแกะข้อมูลครั้งเดียว ถ้าเราเปลี่ยนชื่อฟิลด์ทีหลัง เขาต้องแก้ทั้งระบบ
คาบ 1 เราพูดไว้ว่าหัวใจของ AIoT คือ ตัดสินใจใกล้จุดเกิดเหตุ แล้วส่งขึ้นไปเฉพาะสิ่งที่มีความหมาย วันนี้ถึงเวลาทำจริง
ลองคิดเลขให้เห็นภาพ อ่านค่าทุก 200 ms แล้วส่งทุกค่า
แบบส่งเหตุการณ์: ส่งตอนสถานะเปลี่ยน + heartbeat ทุก 30 วินาที = 2,883 ข้อความต่อวัน ลดลงร้อยกว่าเท่า และปลายทางอ่านง่ายกว่าเดิม
ทำไมยังต้องมี heartbeat: ถ้าเงียบอย่างเดียว ปลายทางแยกไม่ออกระหว่าง "ทุกอย่างปกติ" กับ "บอร์ดตายไปแล้วเมื่อวาน"
ดูเพิ่ม (6 นาที): MQTT Essentials Part 6 — MQTT Topic Best Practices — HiveMQ — 5:50 — การออกแบบลำดับชั้น topic และ wildcard + # ที่ฝั่งรับใช้ query
ความเงียบไม่ใช่ข่าวดี จนกว่าเราจะออกแบบให้ความเงียบมีความหมาย

ดูเพิ่ม (5 นาที): MQTT Essentials Part 10 — Last Will and Testament — HiveMQ — 5:27 — broker ประกาศแทนเราเมื่อเราหายไปแบบไม่บอกกล่าว ซึ่งคือกลไกที่ตอบคำถาม "บอร์ดเงียบไป แปลว่าปกติหรือตาย"
MQTT ถูกออกแบบตั้งแต่ปี 1999 โดยวิศวกรสองคน (Andy Stanford-Clark จาก IBM และ Arlen Nipper) สำหรับงานที่ฟังดูไม่น่าเกี่ยวกับเราเลย: ตรวจวัดท่อส่งน้ำมันกลางทะเลทราย ที่เชื่อมโลกด้วยสัญญาณดาวเทียมซึ่งทั้งช้า ทั้งแพง ทั้งหลุดบ่อย
ข้อจำกัดนั้นเองที่ทำให้โพรโทคอลนี้หน้าตาแบบที่เป็น — ส่วนหัวเล็กมาก มีระดับการรับประกันการส่งให้เลือก และมีแนวคิด "last will" คือข้อความที่ broker จะประกาศแทนเราเมื่อเราหายไปแบบไม่บอกกล่าว
เชื่อมกับวันนี้: ทีมที่ออกแบบ payload ให้เล็กและส่งเป็นเหตุการณ์ กำลังใช้โพรโทคอลตรงตามเจตนาของคนออกแบบเมื่อยี่สิบกว่าปีก่อน ส่วนทีมที่ยิงค่าดิบทุก 200 ms กำลังใช้มันผิดวิธี และจะเจอปัญหาเดียวกับที่ท่อน้ำมันเจอ
เน็ตหลุดกลางงานไม่ใช่กรณีพิเศษ มันคือสภาพปกติของอุปกรณ์ที่ติดตั้งจริง
| สถานการณ์ | demo ทำ | product ต้องทำ |
|---|---|---|
| WiFi หลุด | โปรแกรมตาย หรือค้างรอ | จอวาดต่อ ขึ้นคำว่า offline แล้วนัดลองใหม่ทุก 10 วินาที |
| broker ไม่ตอบ | publish เงียบ ไม่มีใครรู้ |
นับจำนวนครั้งที่ส่งไม่สำเร็จ แล้วโชว์บนจอ |
| เน็ตกลับมา | ต้องรีเซ็ตบอร์ดเอง | ต่อเอง แล้วส่งข้อความบอกว่ากลับมาแล้ว |
| ค่าเซนเซอร์กระโดดวูบเดียว | เตือนทันที คนวิ่งมาดูแล้วไม่เจออะไร | ต้องเกินเกณฑ์ติดกันหลายรอบจึงเปลี่ยนสถานะ |
| เตือนแล้วไม่มีใครอยู่หน้าจอ | ข้อความแวบเดียวแล้วหาย | ค้างสถานะไว้จนมีคนกดรับทราบ |
การตัดสินใจที่ต้องเลือกให้ชัดตั้งแต่วันนี้: ตอนออฟไลน์ ทีมจะ ทิ้ง ข้อมูลหรือ เก็บไว้ส่งทีหลัง
โครงเริ่มต้นเลือกทิ้งแล้วนับไว้ เพราะค่าความเอียงเมื่อสิบนาทีที่แล้วไม่มีประโยชน์กับคนที่กำลังยืนอยู่ใต้ที่นั่งร้าน ถ้าทีมทำเครื่องนับจำนวนชิ้นงาน คำตอบอาจกลับกัน — เก็บไว้ให้ครบสำคัญกว่าความสด
ทั้งสองคำตอบถูกได้ ที่ผิดคือไม่เคยตัดสินใจ แล้วปล่อยให้พฤติกรรมเป็นไปตามบังเอิญ

สิ่งที่มีให้แล้ว (70%) — เฟิร์มแวร์อ่านเซนเซอร์และวาดจอ, โมดูล sensors/dsp/ui/wifi/mqtt, และโครง s12_capstone_starter.py ที่รันครบวงจรได้ตั้งแต่ยังไม่แก้อะไร
งานของทีม (30%) — เลือกโจทย์ · เลือกว่าวัดอะไร · ตั้งเกณฑ์ · ออกแบบสิ่งที่คนหน้างานเห็น · ออกแบบ schema · ตัดสินใจเรื่องออฟไลน์ · เล่าให้คนอื่นเข้าใจใน 10 นาที
30% ของวันนี้ไม่ได้วัดกันที่จำนวนบรรทัด มันวัดกันที่ คุณภาพของการตัดสินใจ ทุกอย่างข้างบนคือการตัดสินใจ ไม่ใช่การพิมพ์
คาบก่อน ๆ เราวัดกันว่า "ทำให้มันทำงานได้ไหม" วันนี้เราวัดว่า "ทำไมถึงทำแบบนี้"
# บน Eva ไม่มี sensors.init() ให้เรียก - คอร์จอถือบัส IMU เรียกแล้วได้ OSError ทันที
# บน Dev Kit CM33 อ่าน IMU ตรงจาก I2C เอง และเฟิร์มแวร์ปลุกมันไว้ตั้งแต่บูต จึงไม่ต้อง init เช่นกัน
smooth = dsp.EMA(alpha=0.2) # กันค่ากระโดดครั้งเดียวจนเตือนผิด
last_value = 0.0
stale = False # รอบนี้อ่านไม่ได้ใช่ไหม - จอต้องบอกความจริงข้อนี้
def read_value():
global last_value, stale
try:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
except OSError:
stale = True # ค่าค้างยังมีประโยชน์ แต่ต้องไม่ถูกโชว์เหมือนค่าสด
return last_value
stale = False
roll, pitch = dsp.tilt(ax, ay, az) # dsp.tilt คืน (roll, pitch) - roll มาก่อน
# ทีมเขียนเอง: เปลี่ยนบรรทัดล่างเป็นปริมาณที่โจทย์ของทีมสนใจจริง ๆ
last_value = smooth.update(abs(roll))
return last_value
read_value() คืน ค่าเดียว ไม่ใช่หกแกน — สามช่องที่เหลือของ canvas ตัดสินจากมัน แสดงมัน และส่งมัน · ธง stale มีไว้เพื่อข้อเดียว: ค่าที่แสดงต้องบอกคุณภาพของตัวเองได้ อุปกรณ์ที่อ่านเซนเซอร์ไม่ได้แล้วโชว์เลขเดิมค้างไว้คือเครื่องที่โกหกคนหน้างาน — ท่าที่ 3 จะเอาธงนี้ขึ้นจอว่า "ค่าค้าง อ่านไม่ได้" · ทีมความสั่นเปลี่ยนสองบรรทัดสุดท้ายเป็นขนาดความเร่งรวม ทีมการเคลื่อนย้ายใช้ sensors.bmm350.heading() โครงที่เหลือไม่ต้องแตะ
ถ้าทีมยังตอบไม่ได้ว่า "ค่าเดียว" ของทีมคืออะไร แปลว่าช่อง Sense ในใบงานยังกรอกไม่เสร็จ
LED_NAMES = gpio.board_info()["led_names"]
def led_named(*names, fallback=0):
for n in names:
if n in LED_NAMES:
return gpio.led(LED_NAMES.index(n))
return gpio.led(fallback)
beacon_lamp = led_named(BEACON_LED if "RGB_GREEN" in LED_NAMES else "LED1") # แดงทั้งสองบอร์ด
เซนเซอร์ — บน Eva Kit ไม่ต้องเรียก sensors.init() และเรียกแล้วจะได้ OSError เพราะคอร์จอเป็นเจ้าของบัสเซนเซอร์ bmi270 capsense pot อ่านได้ทันทีผ่าน snapshot ที่คอร์จอเก็บไว้ให้ (การอ่านครั้งแรกหลังรีเซ็ตบน Eva อาจรอได้ถึงราว 16 วินาที) · บน Dev Kit init() ทำงานได้จริงแต่ก็ไม่ต้องเรียก เพราะเฟิร์มแวร์ปลุกเซนเซอร์ไว้ตั้งแต่บูต และ CM33 อ่าน IMU เองโดยตรง · sensors.snapshot() คืน dict รูปเดียวกันทั้งสองบอร์ด
หลอดไฟเตือนหน้างาน (beacon_lamp) ถูกเลือกตามชื่อ จาก gpio.board_info()["led_names"] ไม่ใช่ตามเลข เพราะดัชนี LED ต่างกันตามบอร์ด: RGB_RED บน Dev Kit เป็นดวงสีแดง ส่วนบน Eva ดวงที่ชื่อ RGB_RED เป็นสีน้ำเงิน (ชื่อกับสีไม่ตรงกัน — เป็นของจริงในเฟิร์มแวร์ อย่าไป "แก้") โครงจึงถามก่อนว่ามี RGB_GREEN ไหม: มี = Dev Kit ใช้ RGB_RED · ไม่มี = Eva ใช้ LED1 ซึ่งเป็นดวงแดง
โค้ดที่รันได้ทั้งสองบอร์ดไม่ได้เกิดจากการ "ไม่พูดถึงบอร์ด" แต่เกิดจากการถามบอร์ดว่ามันมีอะไร แล้วเลือกด้วยชื่อ

def decide(value):
if value > LIMIT: # ไล่จากเข้มที่สุดลงมาเสมอ สลับเมื่อไรจะไม่เข้า ALERT เลย
return "ALERT"
if value > WARN_LIMIT:
return "WARN"
return "OK"
def on_state_change(old, new, value):
pass # ทีมเขียนเอง: ตอนสถานะเปลี่ยนให้เกิดอะไร (นับจำนวนครั้ง จดเวลา สั่งของอย่างอื่น)
การตัดสินอยู่บนบอร์ด ไม่ใช่ที่ปลายทาง เหตุผลเดียวกับคาบ 2: เน็ตหลุดแล้วต้องยังตัดสินได้ และการส่งค่าดิบให้คลาวด์คิดแทนคือจ่ายค่าเน็ตเพื่อรอคำตอบที่ช้ากว่า
on_state_change() แยกเป็นฟังก์ชันต่างหาก เพราะ "สิ่งที่เกิดตอนสถานะเปลี่ยน" เป็นของทีมแต่ละทีม ไม่ใช่ของโครง — ทีมหนึ่งนับจำนวนครั้ง อีกทีมจดเวลาเพื่อคิดระยะเวลารวม
ระดับถัดไปที่เฉลยทำ: บังคับให้ค่าต้องเกินเกณฑ์ ติดกัน 3 รอบ ก่อนจึงเปลี่ยนสถานะจริง (CONFIRM_N) โครงเริ่มต้นเชื่อทันทีที่เห็นค่าเกินครั้งเดียว ซึ่งพอสำหรับให้ไฟล์รันได้ แต่จะโทรตามช่างเพราะรถบรรทุกวิ่งผ่าน
สองระดับใช้สอนได้ แต่ของที่ติดตั้งจริงเกือบทุกตัวมีการยืนยันซ้ำก่อนเชื่อด้วย
lbl_value = ui.Label("--", x=22, y=76, color=COL_TEXT, value=30) # ค่าหลัก
lbl_quality = ui.Label("รอค่าแรก", x=250, y=86, color=COL_DIM, value=16)
bar_value = ui.Bar(x=22, y=134, w=440, h=14, color=COL_RUN,
min=0, max=SCALE_MAX, value=0)
sc_value = ui.Scale(x=22, y=150, w=440, h=48, color=COL_TEXT, min=0, max=SCALE_MAX)
sc_value.ticks(10, 3) # 0 · 15 · 30 · 45 ไม่ทับกัน
led_ok = ui.Led(x=26, y=274, w=34, h=34, color=COL_OK, value=1)
led_warn = ui.Led(x=160, y=274, w=34, h=34, color=COL_WARN, value=0)
led_bad = ui.Led(x=320, y=274, w=34, h=34, color=COL_BAD, value=0)
btn_on = ui.Button("เปิดไฟเตือน", x=498, y=124, w=184, h=46, color=0x1B5E20, value=18)
btn_off = ui.Button("ปิดไฟเตือน", x=498, y=178, w=184, h=46, color=0x37474F, value=18)
สี่ข้อที่หน้าจอนี้ทำ และเป็นเกณฑ์ให้คะแนนหน้าจอของทีมด้วย
| ข้อ | ทำอย่างไรในโค้ดนี้ |
|---|---|
| ค่าที่วัดได้ต้องมาพร้อมพิสัย | ui.Bar วางทับ ui.Scale — ตัวเลข 17.9 ลอย ๆ ไม่บอกว่าสูงไหม แต่แท่งที่อยู่บนไม้บรรทัด 0-45 บอกทันที |
| สถานะต้องเป็นไฟ ไม่ใช่ตัวอักษรสี | ui.Led สามดวง ติดทีละดวง — แปลงภาพเป็นขาวดำแล้วสีตัวอักษรหายหมด ไฟที่ติดกับไฟที่หรี่ยังแยกออก |
| ปุ่มเปิดกับปุ่มปิดต้องแยกกัน | btn_on กับ btn_off คนละตัว — ปุ่มเดียวสลับไปมาบอกไม่ได้ว่าตอนนี้อยู่สถานะไหน คนกดต้องเดา |
| ค่าที่แสดงต้องบอกคุณภาพตัวเอง | lbl_quality ขึ้นว่า "ค่าค้าง อ่านไม่ได้" เมื่อธง stale ถูกยก ไม่ใช่ค้างเลขเดิมไว้เฉย ๆ |
ui.Scale ไม่รับ .value() มันคือไม้บรรทัด ตัวที่ขยับคือ ui.Bar ที่วางทับ (แบบวงกลมมีเข็มจริง — คาบ 6) · ui.Led สั่ง .value(0) แล้ว หรี่ ไม่ใช่หาย ซึ่งตั้งใจ เพราะไฟที่หายไปตอนดับ ทำให้คนดูแยกไม่ออกว่าดับหรือจอเสีย
หน้าจอที่ดีตอบได้ใน 2 วินาทีว่า ตอนนี้ปกติหรือไม่ปกติ ตัวเลขละเอียดเป็นเรื่องรอง — คนหน้างานมองผ่านหน้ากากเชื่อมและถือของอยู่สองมือ
บรรทัด lbl_net มีอยู่เพราะจอต้องบอกความจริงเรื่องการเชื่อมต่อด้วย ไม่ใช่โชว์แต่ตัวเลขสวย ๆ ราวกับทุกอย่างปกติทั้งที่ส่งอะไรไม่ออกมาสิบนาทีแล้ว

ถามตัวเองว่า "ถ้าจอนี้ติดอยู่หน้าเครื่องจักรจริง คนเดินผ่านจะเข้าใจใน 2 วินาทีไหม"
# สร้างพร้อมหน้าจอแล้วซ่อนไว้ ไม่ใช่สร้างตอนกด - แฮนเดิลมีจำกัด และการสร้างของ
# ตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · กล่องวางกลางจอ
box = ui.MsgBox("ปิดไฟเตือน\nไฟหน้างานจะดับทันที", x=112, y=96, w=568, h=136,
color=COL_CARD)
box.hide()
btn_yes = ui.Button("ยืนยัน", x=144, y=248, w=200, h=88, color=0x3A4150, value=20)
btn_no = ui.Button("ยกเลิก", x=376, y=248, w=200, h=88, color=0x3A4150, value=20)
btn_yes.hide()
btn_no.hide()
...
elif ev["handle"] == btn_off.id() and not asking:
asking = True # เปิดไม่ต้องถาม เพราะย้อนกลับได้ทันที
box.show()
btn_yes.show()
btn_no.show()
elif ev["handle"] == btn_yes.id() and asking:
asking = False
beacon(False) # ของจริงกับจอขยับพร้อมกันในฟังก์ชันเดียว
box.hide()
btn_yes.hide()
btn_no.hide()
สองเรื่องที่ต้องรู้ก่อนใช้ ui.MsgBox
ui.Button เท่านั้น ถ้าวางปุ่มของกล่องไว้แล้วรอให้คนกด จะได้ปุ่มตายบนจอ และคนกดจะสรุปว่าเครื่องแฮงก์ — ใช้ ui.Button จริงสองตัวเป็นคำตอบแทนคำยืนยันต้องบอก สิ่งที่จะเกิด ไม่ใช่ถามว่า "ยืนยันไหม" — เทียบสองประโยคนี้ตอนตีสามที่หน้างาน: "ยืนยันหรือไม่" กับ "ไฟหน้างานจะดับทันที" · ทีมที่ทำหน้าจอสั่งงานได้ ต้องตอบให้ได้ว่า "ถ้ากดผิดจะเกิดอะไร และย้อนกลับได้ไหม"
sec = now // 1000 # ประตูเดียว: วินาทีเปลี่ยนหรือยัง
if sec == last_sec:
return # แถบกับไฟขยับไปแล้วข้างบน ส่วนตัวเลขรอรอบหน้า
last_sec = sec
lbl_value.text("{:.1f} {}".format(value, UNIT))
ลูปเดินทุก 200 ms แต่ตัวเลขที่กระพริบห้าครั้งต่อวินาทีอ่านไม่ทัน — แถบกับไฟขยับได้ทุกรอบ เพราะตาอ่านรูปทรงได้เร็วกว่าตัวเลข ส่วนป้ายสถานะเขียนตอน เปลี่ยน ไม่ใช่ตอนถึงรอบวินาที ไม่งั้นป้ายกับไฟจะไม่ตรงกันได้นานถึงหนึ่งวินาที ซึ่งคนดูจะอ่านว่าจอเพี้ยน
สามจังหวะในฟังก์ชัน show() เดียวกัน: ทุกรอบ — แถบค่าและไฟสามดวง · ตอนเปลี่ยน — ป้ายสถานะและป้าย "ค้างอยู่" · วินาทีละครั้ง — ตัวเลขค่า ป้ายคุณภาพ สถานะเน็ต และตัวนับใบที่ส่ง
ตัวเลขที่กระพริบเร็วกว่าคนอ่านทัน ไม่มีใครได้ประโยชน์จากมัน — และตำแหน่งของมันต้องคงที่เสมอ
def send(value, state, kind):
if not mqtt.is_connected(): # เช็กสายก่อนส่งเสมอ
return False
mqtt.publish(TOPIC, payload(value, state, kind))
return True
def go_online():
if not wifi.is_connected():
if not wifi.connect(WIFI_SSID, WIFI_PASS):
return False
return mqtt.connect(BROKER, port=1883, client_id=DEVICE_ID)
if online and not mqtt.is_connected():
online = False
t_retry = now
if (not online) and time.ticks_diff(now, t_retry) >= RETRY_MS:
online = go_online() # นัดเวลาลองใหม่ ไม่ต่อรัว ๆ ในลูป
t_retry = now
สามอย่างที่ต้องสังเกต: send() คืน True/False ให้ผู้เรียกรู้ผล ไม่ใช่เงียบหาย · การต่อใหม่ถูกนัดเวลาไว้ ไม่ใช่พยายามทุกรอบลูป · และไม่ว่าเน็ตเป็นอย่างไร ลูปยังเดินครบทุกรอบ
client_id=DEVICE_ID สำคัญกว่าที่คิด — บน broker สาธารณะ ถ้าสองบอร์ดใช้ client id เดียวกัน จะเตะกันหลุดสลับไปมาเป็นลูป
ยกระดับได้ด้วย
tesaiot.connect()ของคาบ 11 (TLS 8884) แต่ต้อง provision ตัวตนอุปกรณ์รายทีมก่อน จึงไม่อยู่ในโครงเริ่มต้น
ดูเพิ่ม (6 นาที): MQTT Essentials Part 7 — Quality of Service — HiveMQ — 5:41 — QoS 0/1/2 คือคำตอบระดับโพรโทคอลของคำถามเดียวกันนี้ และราคาที่ต้องจ่ายของแต่ละระดับ
จอกับ broker ต้องเป็นเส้นทางที่แยกกันได้ ถ้าฝั่งหนึ่งล้ม อีกฝั่งต้องไม่ล้มตาม
practise_codes/s12_capstone_starter.py↗ ใน BENTO IDEDEVICE_ID, WIFI_SSID, WIFI_PASS, TOPIC (ใช้ bento/teamNN/... ของทีมเท่านั้น กันชนกับทีมอื่นบน broker สาธารณะ)bento/teamNN/# เพื่อดูข้อความของทีมkind: event ขึ้น brokerถ้าเชื่อมต่อไม่ผ่าน wifi.connect() อาจบล็อกอยู่นาน อย่าเพิ่งรีบกดรันซ้ำ รอให้มันตอบก่อน
รันโครงให้ผ่านตั้งแต่ยังไม่แก้อะไร คือการพิสูจน์ว่าพื้นดีก่อนขึ้นบ้าน
ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 28 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · แยกค่าที่วัดได้ออกจากสถานะ · 10 นาที | examples/s12/01_state_machine.py↗ |
วางช่อง Decide ของ canvas ทีมได้เป็นโครงจริง ไม่ใช่ if กระจายทั้งไฟล์ · จะเข้าใจว่าค่าที่วัดได้กับสถานะที่ตัดสินแล้วเป็นคนละของ ต้องแยกกันอยู่ |
| 2 · ยืนพื้นครบ N รอบก่อนจึงเชื่อ · 10 นาที | examples/s12/02_confirm_n.py↗ |
กันไม่ให้ค่ากระตุกวูบเดียวยิง alert ตอนตีสอง ซึ่งเป็นข้อที่กรรมการถามแน่ · จะเข้าใจว่าระดับใหม่ต้องยืนพื้นครบ N รอบก่อน จึงจะเชื่อได้ว่ามันเปลี่ยนจริง |
| 3 · ต่อใหม่แบบถอยห่างขึ้นเรื่อย ๆ · 8 นาที | examples/s12/03_reconnect_backoff.py↗ |
ทำให้ demo รอดตอนเน็ตห้องสะดุด และตอบได้ว่าทำไมไม่ต่อใหม่ทุกวินาที · จะเข้าใจว่าการต่อใหม่ต้องถอยห่างขึ้นเรื่อย ๆ ไม่ใช่รัวเท่าเดิมทุกครั้ง |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| ถอดเราเตอร์แล้วจอค้างไปด้วยทั้งเครื่อง | examples/s12/05_hmi_survives_offline.py↗ — แยกลูปจอออกจากลูปเครือข่าย จอต้องเดินต่อได้ |
| alert ยิงถี่จน broker เต็มและคนเลิกอ่าน | examples/s12/04_heartbeat_and_alert.py↗ — heartbeat กับ alert คนละจังหวะ พร้อมช่วงเว้นที่นับได้ |
| อยากให้แหล่งค่าเป็นเสียง ไม่ใช่ค่าจากเซนเซอร์ตรง ๆ | examples/s08/02_mic_sound_level_meter.py↗ — mic.level() ยุบคลื่นเสียงทั้งชุดเหลือตัวเลขเดียว แล้วครอบด้วย confirm-N ได้เหมือนกัน |
| ตอน demo จอนิ่งอยู่เฉย ๆ แล้วตอบกรรมการไม่ได้ว่าโปรแกรมยังวิ่งอยู่หรือค้างไปแล้ว | examples/usecase/02_heartbeat_liveness.py↗ — ไฟที่กะพริบเป็นจังหวะพิสูจน์ว่าลูปยังหมุน ส่วนไฟที่ติดค้างพิสูจน์ได้แค่ว่ามีไฟเลี้ยง · จังหวะนับจากนาฬิกา ไม่ใช่จาก sleep ยาว ๆ ที่ยึดลูปไว้ |
อ่านเสริมนอกเวลา — เรื่องนี้เป็นไฟล์ของคาบ 7 ไม่ใช่ชิ้นส่วนของโครงวันนี้ และไม่อยู่ในเกณฑ์ผ่านของคาบ 12: examples/s07/01_imu_vibration_monitor.py↗ เป็นโจทย์ capstone ที่ทำจบได้จริงในหนึ่งคาบ — เฝ้าการสั่นเทียบเส้นฐานที่วัดเอง แล้วรายงานเป็นกี่เท่าของเส้นฐาน · เปิดตอนเลือกโจทย์ยังไม่ลงตัว
โจทย์ที่ต้องฟังเสียงทำได้แล้ว โมดูล mic เปิดไมโครโฟนจาก Python ได้ทั้งบนบอร์ดและในอีมูเลเตอร์ ทีมที่อยากให้แหล่งค่าเป็นเสียงจึงเอา mic.level() หรือ mic.peak() ไปเสียบช่อง Sense ของ canvas ได้ตรง ๆ · กฎ confirm-N ในไฟล์ที่ 2 ข้างบนใช้ครอบค่าจากไมค์ได้เหมือนกับค่าจากเซนเซอร์ตัวอื่นทุกประการ ตัวอย่างไมค์สองไฟล์อยู่ที่คาบ 8
demo end-to-end + นำเสนอ 10 นาที (ปัญหา→สถาปัตยกรรม→demo→ข้อจำกัด)
แปลเป็นสิ่งที่ตรวจได้จริง:
ข้อที่ห้าถึงเจ็ดคือข้อที่แยกทีมที่ทำ product ออกจากทีมที่ทำ demo
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| จอค้าง widget หายไปพักหนึ่ง | ลืม ui.poll() ในลูป |
เรียก ui.poll() ทุกรอบก่อน sleep_ms |
| ค่าบนจอวิ่งกระตุก เฟรมหาย | ลูปเร็วเกินไป | time.sleep_ms(200) สำหรับหน้าจอหนัก |
| ตัวอักษรเล็กใหญ่ผิดคาด | เข้าใจว่า value= ของ Label คือค่าที่แสดง |
value= คือขนาดฟอนต์ (14/16/20/24/28) ใช้ .text() แสดงค่า |
| เตือนถี่จนไม่มีใครสนใจ | ส่งทุกครั้งที่ค่าเกินเกณฑ์ | ส่งตอนสถานะ เปลี่ยน + เว้นระยะขั้นต่ำ + ยืนยันหลายรอบ |
| ข้อความของทีมอื่นโผล่มาปนกัน | ใช้ topic ซ้ำกัน หรือ client_id ซ้ำ |
ใช้ bento/teamNN/... และ client_id ของทีมเสมอ |
| บอร์ดหลุดจาก broker สลับไปมา | สองบอร์ดใช้ client_id เดียวกัน |
ตั้ง DEVICE_ID ให้ไม่ซ้ำ |
| โปรแกรมค้างนานตอนเริ่ม | wifi.connect() บล็อกอยู่ (รหัสผิดจะนานมาก) |
ตรวจรหัสก่อน แล้วรอให้มันตอบ อย่ารันซ้ำทับ |
sensors อ่านค่าไม่ขยับหลังใช้ ui |
การใช้ ui.* ครั้งแรกหยุด sensor auto-task |
อ่านเซนเซอร์แบบ sync ในลูปเอง (โครงทำให้แล้ว) |
| ค่ากระโดดเป็นศูนย์ทุกครั้งที่จอวาด | เผลอเรียก sensors.scan() |
ห้ามใช้ sensors.scan() เด็ดขาดในคอร์สนี้ |
กดปุ่มในกล่อง ui.MsgBox แล้วไม่มีอะไรเกิดขึ้น |
ปุ่มในตัว MsgBox ยังไม่ส่งเหตุการณ์กลับมาให้ Python เห็น | ใช้ ui.Button จริงสองตัวเป็นคำตอบ สร้างแล้ว .hide() ไว้ แล้ว .show() ตอนถาม |
| ข้อความในกล่องยืนยันขาดครึ่ง | ข้อความของ MsgBox เดินทางกับ CREATE ซึ่งพาได้ 95 ไบต์ ไทยตัวละ 3 ไบต์ | เขียนให้สั้นกว่า 31 ตัวอักษร แล้วเปิดภาพดูจริงว่าไม่ถูกตัด |
เปลี่ยนสีแท่ง ui.Bar แล้วดูเหมือนแท่งเต็มทั้งราง |
.color() ของ Bar ไปลงที่รางไม่ใช่แถบที่เต็ม |
อย่าเปลี่ยนสีแท่งตอนรัน ให้ ui.Led หรือสีตัวเลขเป็นคนบอกสถานะ |
| ป้ายสถานะกับไฟบนจอไม่ตรงกันชั่วครู่ | ป้ายถูกกั้นด้วยประตูวินาที แต่ไฟขยับทุกรอบ | เขียนป้ายสถานะตอน "เปลี่ยน" ส่วนประตูวินาทีใช้กับตัวเลขเท่านั้น |
กับดักครึ่งหนึ่งในตารางนี้เป็นเรื่องการออกแบบจังหวะเวลา ไม่ใช่เรื่องไวยากรณ์ภาษา — และสี่ข้อล่างสุด ไม่มี error ให้จับสักตัว มีแต่จอที่ผิด
# ทีมเขียนเอง: เปลี่ยนบรรทัดล่างเป็นปริมาณที่โจทย์ของทีมสนใจจริง ๆ
return smooth.update(abs(roll))
def on_state_change(old, new, value):
pass # ทีมเขียนเอง: ตอนสถานะเปลี่ยนให้เกิดอะไร
# ทีมเขียนเอง: เพิ่ม widget ของทีม (งบรวมทั้งจอไม่เกิน 64 ตัว ตอนนี้ใช้ไป 31)
# ทีมเขียนเอง: เพิ่มหรือตัดฟิลด์ให้ตรงกับตาราง schema ที่ทีมออกแบบไว้ในใบงาน
pass # ทีมเขียนเอง: ออฟไลน์แล้วจะ "ทิ้ง" หรือ "เก็บไว้ส่งทีหลัง"
ลำดับที่แนะนำ: หนึ่ง รันโครงเปล่าให้ผ่านก่อน สอง เปลี่ยน read_value() เป็นของทีม สาม ตั้งเกณฑ์ใน CONFIG แล้วทดสอบด้วยมือจริงว่ามันเตือนตอนที่ควรเตือน สี่ แต่งหน้าจอ ห้า ปรับ schema หก ทดสอบตอนเน็ตหลุด
ห้ามข้ามข้อสาม: ทีมที่ตั้งเกณฑ์จากการเดาแล้วไม่เคยทดสอบ จะไปเจอตอนนำเสนอหน้าห้องว่ามันไม่เตือน
เกณฑ์ที่ยังไม่เคยถูกทดสอบด้วยมือ ไม่ใช่เกณฑ์ มันคือความหวัง
| ช่วง | เวลา | สิ่งที่ต้องพูด | ที่คนมักพลาด |
|---|---|---|---|
| 1 · ปัญหา | 2 นาที | ใครเดือดร้อน เดือดร้อนยังไง วันนี้เขาแก้ยังไงอยู่ | เริ่มด้วยการอวดบอร์ดแทนการเล่าปัญหา |
| 2 · สถาปัตยกรรม | 2 นาที | canvas ห้าช่อง + schema ที่ส่งจริง | ไล่อธิบายโค้ดทีละบรรทัด |
| 3 · demo สด | 4 นาที | ทำให้มันเตือนต่อหน้าคนดู + โชว์ข้อความบน broker + ตัดเน็ตให้ดู | ฉายวิดีโอที่อัดไว้แทนของจริง |
| 4 · ข้อจำกัดและก้าวต่อไป | 2 นาที | สิ่งที่ระบบนี้ยังทำไม่ได้ และถ้ามีเวลาอีกสองสัปดาห์จะทำอะไร | บอกว่า "ไม่มีข้อจำกัด" |
เตรียม แผนสำรอง ไว้เสมอ: WiFi ห้องเรียนล่มตอนนำเสนอเป็นเรื่องที่เกิดขึ้นจริง ทีมที่ออกแบบ offline mode ไว้ดี จะเปลี่ยนอุบัติเหตุนั้นให้กลายเป็นจุดขายของตัวเองได้ทันที
ซ้อมจับเวลาอย่างน้อยหนึ่งรอบ สิบนาทีสั้นกว่าที่ทุกทีมคิดเสมอ
ช่วงที่ 4 คือช่วงที่กรรมการดูว่าทีมเข้าใจงานตัวเองจริงไหม ห้ามข้าม
| หัวข้อ | ผ่าน (2 คะแนน) | ยังไม่ผ่าน (0-1) |
|---|---|---|
| ปัญหาชัด | บอกได้ว่าใครเดือดร้อน วัดความเดือดร้อนเป็นเวลาหรือเงินได้ | เล่าแต่ว่าทำอะไร ไม่บอกว่าเพื่อใคร |
| สถาปัตยกรรมอ่านออก | คนฟังวาด canvas ห้าช่องตามได้ | กระโดดเข้าโค้ดทันที |
| schema สมเหตุผล | ทุกฟิลด์อธิบายได้ว่าปลายทางใช้ทำอะไร มีหน่วยครบ | ส่งค่าดิบทุกอย่างเพราะ "เผื่อไว้" |
| demo สดผ่าน | ระบบเตือนได้จริงต่อหน้าคนดู และเห็นข้อความขึ้น broker | เปิดวิดีโอที่อัดไว้ |
| ทดสอบการพัง | ตัดเน็ตให้ดู แล้วอธิบายพฤติกรรมที่ออกแบบไว้ | ไม่เคยลอง |
| รู้ข้อจำกัดตัวเอง | บอกได้อย่างน้อย 2 ข้อ พร้อมเหตุผลเชิงเทคนิค | ตอบว่าใช้งานได้ทุกกรณี |
| ตรงเวลา | จบใน 10 นาที ครบทั้งสี่ช่วง | เกินเวลา หรือรีบข้ามช่วงที่ 4 |
การซ้อมที่ได้ผลที่สุด: ให้เพื่อนอีกทีมฟังก่อนหนึ่งรอบ แล้วให้เขาเล่ากลับมาว่าเข้าใจว่าทีมเราทำอะไร ถ้าเขาเล่าผิด แปลว่าเรายังเล่าไม่ชัด ไม่ใช่เขาฟังไม่เป็น
ตารางนี้อยู่ในใบงานด้วย ติ๊กให้ครบก่อนขึ้นพูดจริง
solution_codes/s12_capstone_starter.py↗ ทำโจทย์ที่ 6 (เตือนการเอียงของนั่งร้าน) จนจบ นี่คือ คำตอบหนึ่งที่เป็นไปได้ ไม่ใช่คำตอบเดียว
base_roll = 0.0
base_pitch = 0.0
for _ in range(10):
r, p = raw_tilt()
base_roll += r / 10.0
base_pitch += p / 10.0
time.sleep_ms(100)
def read_value():
roll, pitch = raw_tilt()
dr = roll - base_roll
dp = pitch - base_pitch
return smooth.update((dr * dr + dp * dp) ** 0.5) # เอียงไปทางไหนก็นับเป็นการเอียง
สองวินาทีแรกของโปรแกรมใช้จำ "ท่าตั้งต้น" ของอุปกรณ์ เพราะไม่มีใครติดตั้งอะไรได้ระนาบ 0 องศาเป๊ะ ถ้าไม่หักค่าตั้งต้น อุปกรณ์ที่ติดเอียง 6 องศาตั้งแต่วันแรกจะเตือนตลอดไปโดยไม่มีอะไรผิด
การรวม roll กับ pitch ด้วยระยะทางแบบพีทาโกรัส ทำให้ "เอียงไปทางไหนก็นับ" — ถ้าใช้แค่ roll ตัวเดียว นั่งร้านที่เอียงไปข้างหน้าจะรอดสายตาไปเฉย ๆ
ค่าที่วัดเทียบกับตัวเองตอนติดตั้ง มีความหมายกว่าค่าที่วัดเทียบกับแรงโน้มถ่วงเสมอ
if level == pending:
streak += 1
else:
pending = level
streak = 1
if streak >= CONFIRM_N and pending != state:
state = pending
if state == "ALERT":
latched = True
beacon(True) # ของจริงขยับเอง ไม่ต้องรอคนกด
ready = last_alert is None or time.ticks_diff(now, last_alert) >= ALERT_GAP_MS
if ready:
if send(value, state, "event"):
last_alert = now
else:
missed += 1
สามกลไกซ้อนกันอยู่ตรงนี้ และแต่ละอันแก้ปัญหาคนละเรื่อง
ยืนยัน 3 รอบ (CONFIRM_N) กันการสั่นวูบเดียวตอนมีคนเดินชน — 3 รอบ × 200 ms คือหกในสิบวินาที นานพอให้ของจริงยังเกินอยู่ แต่สั้นพอที่จะไม่ช้า
ค้างสถานะ (latched) เพราะเหตุการณ์ที่ไม่มีใครเห็นเท่ากับไม่เคยเกิด สถานะจะค้างจนมีคนกดปุ่มรับทราบบนจอ · บรรทัด beacon(True) ทำสองอย่างพร้อมกันในฟังก์ชันเดียว คือ จุดหลอดจริงบนบอร์ดและจุดไฟบนจอ — จอที่บอกว่าไฟติดทั้งที่หลอดดับ คือจอที่โกหก และวิธีเดียวที่กันได้คือให้ทั้งสองอย่างออกจากบรรทัดเดียวกันเสมอ
เว้นระยะขั้นต่ำ (ALERT_GAP_MS) กันการเตือนรัวตอนค่าแกว่งรอบเกณฑ์ ระบบที่เตือนสามสิบครั้งใน 1 นาที จะถูกคนหน้างานปิดเสียงในสัปดาห์แรก
ระบบเตือนภัยที่คนเลือกจะไม่ฟัง แย่กว่าไม่มีระบบเตือนภัย เพราะมันสร้างความมั่นใจปลอม ๆ
if (not online) and time.ticks_diff(now, t_retry) >= RETRY_MS:
online = go_online()
t_retry = now
if online:
if send(value, state, "back"): # กลับมาแล้วบอกฝั่งรับทันที อย่าให้เขาเดา
sent += 1
...
for ev in ui.poll(): # ต้องเรียกทุกลูป ไม่งั้นจอจะซ่อน widget ราวสองวินาที
if ev["type"] != "clicked":
continue
if ev["handle"] == btn_on.id():
beacon(True) # เปิดไม่ต้องถาม ย้อนกลับได้ด้วยปุ่มข้าง ๆ
elif ev["handle"] == btn_off.id() and not asking:
asking = True # ปิดต้องถาม เพราะมันลบการเตือนของจริงทิ้ง
box.show()
...
elif ev["handle"] == btn_yes.id() and asking:
asking = False
beacon(False)
...
elif ev["handle"] == btn_ack.id() and latched:
latched = False
if send(value, state, "ack"):
sent += 1
ปุ่มทั้งห้าตัวอ่านจาก ui.poll() เดียวกัน และไม่มี widget ตัวไหนถูกสร้างในลูปเลย — กล่องยืนยันกับปุ่มคำตอบถูกสร้างพร้อมหน้าจอแล้วซ่อนไว้ตั้งแต่ต้น (สไลด์ "ท่าที่ 3 ต่อ") การสร้างของตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · send() คืน False เมื่อสายหลุด จึงนับ sent เฉพาะใบที่ออกไปจริง — ตัวเลขบนจอต้องไม่โกหก
กดรับทราบแล้วส่ง
ack· กลับมาออนไลน์แล้วส่งback— ฝั่งรับต้องไม่ต้องเดาว่าบอร์ดหายไปไหน
ท่า 1 Sense มาก่อน เพราะถ้าค่าที่อ่านยังเชื่อไม่ได้ ทุกอย่างที่สร้างทับบนมันคือการตกแต่งความผิดพลาด
ท่า 2 Decide มาที่สอง เพราะสถานะเป็นสิ่งที่ทั้งจอและ broker ใช้ร่วมกัน ตัดสินให้จบที่เดียวก่อน แล้วอีกสองฝั่งค่อยไปหยิบใช้ — ไม่ใช่ต่างคนต่างคิดแล้วได้คำตอบไม่ตรงกัน
ท่า 3 Show มาก่อน Send เพราะจอไม่พึ่งเน็ต ถ้าเรียงกลับกัน ทีมมักเผลอเขียนโค้ดที่จอจะอัปเดตก็ต่อเมื่อส่งสำเร็จ ซึ่งเป็นบั๊กที่หาเจอยากมากตอนสาย WiFi ดี
ท่า 4 Send มาที่สี่ เพราะมันคือส่วนที่พึ่งพาสิ่งที่เราควบคุมไม่ได้มากที่สุด
ท่า 5 กันเน็ตหลุด มาสุดท้าย เพราะจะเขียนได้ ต้องรู้ก่อนว่ามีอะไรจะพังบ้าง — และมันคือส่วนที่ทีมส่วนใหญ่ไม่มีเวลาเขียน ถ้าไม่วางไว้ในลำดับตั้งแต่ต้น
เรียงตามความน่าเชื่อถือจากมากไปน้อย: เซนเซอร์ → ตรรกะ → จอ → เน็ต
examples/s12/06_sense_decide_act_report.py↗ ต่อห้าท่าที่ฝึกมาให้เป็นวงเดียว และรันได้แม้ไม่มีเน็ต
วัด → ตัดสิน → สั่งของจริง → รายงาน แล้ววนกลับ · บนจอมีแถบสี่ช่องที่สว่างทีละช่องตามขั้นที่กำลังทำ ผู้เรียนจึงเห็นวงจรเดินด้วยตา ไม่ต้องจินตนาการ
ขั้นที่ 1 วัดอุณหภูมิ ผ่าน read_temp() ในไฟล์ — sensors.snapshot() ไม่มีช่องอุณหภูมิบนบอร์ดไหนเลย ไฟล์จึงถามก่อนว่ามี sensors.sht40 ไหม: บน Dev Kit ได้อุณหภูมิห้องจริง ส่วน บน Eva ลูกบิดเล่นบทแทน (0–100 % = 15–45 °C) และ console บอกไว้ว่าค่ามาจากไหน — หมุนข้ามเกณฑ์ได้ในห้องเรียน · ขั้นที่ 3 สั่งของจริง คือหลอดที่เลือกตามชื่อด้วย led_named("RGB_GREEN", "LED2") — เขียวทั้งสองบอร์ด ตรงกับป้าย "เปิด" บนจอ
ขั้นที่ 2 คือขั้นที่ทีมส่วนใหญ่ทำพลาด — ถ้าใช้เกณฑ์ค่าเดียว ค่าที่แกว่งรอบเกณฑ์พอดีจะสั่งเปิดปิดสลับกันหลายครั้งต่อวินาที รีเลย์จริงพังด้วยวิธีนี้ และไม่มี error ให้จับสักตัว ไฟล์นี้จึงใช้สองเกณฑ์ เปิดที่ 27.5 ปิดที่ 26.5 ช่องว่างหนึ่งองศาระหว่างสองค่าคือสิ่งที่กันไว้
ข้อสอบของโครงนี้อยู่ที่ท้ายไฟล์ — เปลี่ยนขั้นที่ 1 จากอ่านอุณหภูมิไปอ่านเสียง mic.level() โดยไม่แตะขั้น 2 ถึง 4 เลย ถ้าแก้ที่เดียวจบ แปลว่าทีมแยกส่วนถูกต้องแล้ว และเปลี่ยนโจทย์ได้โดยไม่ต้องเขียนใหม่ทั้งไฟล์
โครงนี้ใช้กับงานจบของทุกทีมได้ — เปลี่ยนแค่ว่า วัดอะไร · ตัดสินด้วยกฎอะไร · สั่งอะไร
สิ่งที่ติดตัวไปแม้เปลี่ยนบอร์ดเปลี่ยนภาษา: การเริ่มจากปัญหาไม่ใช่จากเทคโนโลยี · การแยกการตัดสินใจออกจากการแสดงผลและการส่งข้อมูล · การออกแบบพฤติกรรมตอนพังตั้งแต่ต้น ไม่ใช่ตอนเจอปัญหา · การสื่อสารคุณค่าให้คนที่ไม่ได้เขียนโค้ดเข้าใจ
ดูเพิ่ม (25 นาที · ดูเป็นการบ้าน): Build Real-Time IoT Dashboard: Node-RED + InfluxDB + Grafana + MQTT — IoT Frontier — 24:54 — ปลายทางของเส้นทางที่ 4 หน้าตาเป็นอย่างไรเมื่อมีอุปกรณ์หลายตัวและต้องเก็บย้อนหลัง
เครื่องมือจะเปลี่ยนทุกสามปี วิธีคิดสี่ข้อข้างบนอยู่กับเราได้ทั้งอาชีพ
ทั้งสี่มุมใช้โครงโปรแกรมเดียวกับที่ทีมกำลังจะเขียนวันนี้ ต่างกันที่โจทย์และเกณฑ์เท่านั้น
เส้นทาง 1 · ยกระดับความปลอดภัยของผลงานตัวเอง
เปลี่ยนจาก mqtt พอร์ต 1883 เป็น tesaiot.connect() พอร์ต 8884 ที่เข้ารหัส TLS (ต้อง provision ตัวตนอุปกรณ์รายทีมก่อน) แล้วอธิบายให้ได้ว่า serverTLS ปกป้องอะไร และไม่ปกป้องอะไร
เส้นทาง 2 · ให้คนหน้างานตั้งค่าเองได้ โดยไม่ต้องแก้โค้ด
ทุกไฟล์ในคอร์สนี้พิมพ์ WIFI_SSID กับรหัสผ่านค้างไว้บนหัวไฟล์ ซึ่งใช้ได้ในห้องเรียนแต่ใช้ไม่ได้กับของที่ส่งมอบจริง — ช่างที่ไปติดตั้งไม่มีทั้งคอมพิวเตอร์และรหัสผ่าน Wi-Fi ของลูกค้าอยู่ในมือตั้งแต่แรก เส้นทางนี้ใช้ของที่เรียนไปแล้วทั้งหมด: wifi.scan() ไล่ดูว่ามีวงอะไรอยู่แถวนั้น wifi.softap() เปิดวงของบอร์ดเองให้ช่างต่อเข้ามาด้วยมือถือ แล้ว tesaiot.config_set() เก็บสิ่งที่ช่างกรอกลงแฟลช ครั้งต่อไปบอร์ดต่อเองได้เลย นี่คือขั้นตอนที่เครื่องมือช่างและกล้องติดรถทุกยี่ห้อทำกัน และไม่ต้องใช้ API ตัวใหม่แม้แต่ตัวเดียว
เส้นทาง 3 · ทำให้มันอยู่ได้เป็นเดือน
ของที่ติดตั้งจริงต้องคิดเรื่องไฟ การกู้คืนตัวเองเมื่อค้าง การอัปเดตเฟิร์มแวร์จากระยะไกล และการดูแลอุปกรณ์เป็นร้อยตัวพร้อมกัน — นี่คือวิศวกรรมคนละครึ่งของงานที่เราเพิ่งทำ
เส้นทาง 4 · ขยายจากหนึ่งตัวเป็นฝูง
สิบบอร์ดในโรงงานเดียวกันต้องมี topic ที่ออกแบบมาให้ query ได้ มี dashboard รวม และมีวิธีบอกว่าตัวไหนเงียบไปตั้งแต่เมื่อไร ลองออกแบบโครงสร้าง topic สำหรับ 50 อุปกรณ์ดู แล้วจะเห็นว่าทำไมชื่อ topic ถึงสำคัญ
เลือกหนึ่งเส้นทาง เขียนลงใบงานข้อ 8 แล้วลงมือสัปดาห์นี้ — ความตั้งใจที่ไม่มีวันเริ่ม จะไม่เคยเริ่ม
ui.SpanGroup
examples/s12/07_spangroup_event_log.py↗ สร้าง เหตุการณ์ในภาพเป็นชุดที่ไฟล์นั้นกำหนดไว้เอง ไม่ใช่เหตุการณ์ที่บอร์ดเจอจริงสไลด์ "ส่งเหตุการณ์ ไม่ใช่สตรีมดิบ" ของคาบนี้บอกว่าอะไรควรส่งขึ้นไป — และของชุดเดียวกันนั้นควรอยู่บนจอด้วย แต่พอเขียนจริง ทุกทีมทำเหมือนกันหมด คือ Label หลายบรรทัดแล้วเปลี่ยนสีข้อความตามความรุนแรง ซึ่งตกเกณฑ์ §S7.7.1 ทันที
| วิธีบอกความรุนแรง | ผ่านการทดสอบขาวดำไหม |
|---|---|
| สีข้อความอย่างเดียว | ไม่ผ่าน — แปลงเป็นเกรย์สเกลแล้วสามสถานะกลายเป็นเทาเหมือนกันหมด |
| ขนาดตัวอักษร | ผ่าน |
| เส้นใต้ / เส้นขีดกลาง | ผ่าน |
| สี บวก อย่างใดอย่างหนึ่งข้างบน | ผ่าน และคนตาบอดสีอ่านได้ด้วย |
ลองเอง 30 วินาที — ถ่ายจอด้วยมือถือแล้วเปิดโหมดขาวดำ ถ้ายังแยกสามระดับออก หน้าจอนั้นผ่าน §S7.7.1
ui.SpanGroup — ท่อน ปากกา และแฮนเดิลที่ไม่มีui.SpanGroup คือย่อหน้าเดียวที่ประกอบจาก "ท่อน" หลายท่อน แต่ละท่อนมีขนาด สี และเส้นใต้ของตัวเองได้ จึงใส่ทั้งสองช่องทางลงในบรรทัดเดียวได้โดยไม่ต้องเปลือง widget — สามท่อนต่อหนึ่งเหตุการณ์ใน examples/s12/07_spangroup_event_log.py↗:
color, decor = LEVEL[level] # decor ของสองระดับบนคือ ui.SPAN_UNDERLINE
log.pen(COL_DIM)
log.add_span("%3d s " % at, 20) # เวลา ตัวเล็ก สีจาง
log.pen(color)
log.add_span(level, 24, decor) # ระดับ สี + เส้นใต้
log.pen(COL_TEXT)
log.add_span(" " + text + "\n", 24)
.pen() เป็นปากกา ไม่ใช่คำสั่งทาสีของที่มีอยู่แล้ว — มีผลกับท่อนที่เติมหลังจากนั้นเท่านั้น ลืม pen ท่อนที่สามแล้วทั้งบรรทัดจะเป็นสีของระดับ
ท่อนไม่มีแฮนเดิล จึงแก้ทีละท่อนไม่ได้ ต้อง .clear_items() แล้วเขียนใหม่ทั้งย่อหน้า ซึ่งบังคับให้ "ความจริง" อยู่ในตัวแปรฝั่ง MicroPython — หลักเดียวกับที่ใช้มาทั้งคอร์ส คือมีที่เดียวที่มีสิทธิ์เปลี่ยนสถานะ
เวลาตัวเล็กสีจาง · ระดับสีตามความรุนแรงพร้อมเส้นใต้ · เนื้อความสีปกติ — สองช่องทางในบรรทัดเดียว คือสิ่งที่
Labelหลายบรรทัดทำไม่ได้
ui.Calendar
examples/s12/08_calendar_sets_the_clock.py↗ สร้าง วันที่ 2015-01-01 บนจอคือค่าที่นาฬิกาของบอร์ดตอบจริงหลังเปิดเครื่อง ไม่ได้พิมพ์ไว้ในสไลด์ย้อนกลับไปที่ฟิลด์ t ใน schema ของโครงเริ่มต้น — สไลด์เขียนไว้ว่า "เวลาบนบอร์ด ใช้เรียงลำดับ" ไม่ได้เขียนว่าวันที่ เพราะมันไม่ใช่ t คือ ticks_ms ซึ่งนับจากตอนเปิดเครื่อง
เหตุผลอยู่ในเฟิร์มแวร์: machine_rtc.c ตั้ง RTC_INIT_YEAR เป็น 15 บนฐานปี 2000 และคอมเมนต์ของ reset เขียนว่า "Resets RTC to 1st Jan' 2015" — ทุกครั้งที่ตัดไฟ นาฬิกาของบอร์ดกลับไปที่ 1 ม.ค. 2015 เสมอ ทีมที่ส่งวันที่ขึ้น platform โดยไม่ได้ตั้งนาฬิกา จะได้ข้อมูลปี 2015 ทั้งชุด และไม่มีใครสังเกตจนกว่าจะเอาไปทำกราฟย้อนหลัง
| ที่มาของวันที่ | มีบนโต๊ะทดลองนี้ไหม |
|---|---|
| NTP ผ่านอินเทอร์เน็ต | ต้องมีเน็ต และตั้งเองไม่ได้จาก MicroPython ในพอร์ตนี้ |
| เวลาที่ platform แนบมากับข้อความ | ได้ แต่เป็นเวลาของปลายทาง ไม่ใช่ของบอร์ด |
| คนบอกบอร์ดผ่านหน้าจอ | ได้ทันที และเป็นทางเดียวที่ไม่ต้องพึ่งใคร |
ปี 2015ในข้อมูลของทีมไหน คือลายเซ็นของนาฬิกาที่ไม่เคยถูกตั้ง — จำหน้าตาของมันไว้
ui.Calendar — สองอย่างที่ผิดคาดและต้องรู้ui.Calendar คือ widget ของงาน "คนบอกบอร์ดผ่านหน้าจอ" — และมันสร้างวันที่ผิดปฏิทินไม่ได้ ต่างจากการวาง Spinbox สามช่องซึ่งยอมให้ป้อน 31 กุมภาพันธ์
min max value ที่นี่ไม่ใช่ช่วงค่า อย่าง Slider หรือ Bar แต่คือ ปี · เดือน · วัน (ui_widget_mgr.c บรรทัด 2117 ใน BENTO-TESAIoT-libraries/claw) ใส่ค่านอกพิสัยแล้วมันเงียบ ๆ ถอยไปใช้ 2026-01-01 ไม่มี errorlv_calendar_get_pressed_date() ที่ตอบไม่ผ่านเมื่อไม่ได้แตะวัน — รู้ได้เฉพาะตอนคนแตะวัน ไม่ใช่ตอนคนพลิกดูเดือน · ค่าที่ส่งมาคือเลขแปดหลัก YYYYMMDD ก้อนเดียว ต้องแกะเอง
machine.RTCมีอยู่จริงทั้ง Eva Kit และ Dev Kit (ตารางmodmachine.cของพอร์ตร่วมใส่ไว้โดยไม่มีเงื่อนไข) ต่างจากmachine.PWMและmachine.ADCที่ไม่มี — เรียกได้เลยโดยไม่ต้องtry/except



examples/usecase/ — ไม่ได้อยู่ในคาบไหนโดยเฉพาะ หยิบไปใช้กับงานจบได้เลย

examples/usecase/ — ไม่ได้อยู่ในคาบไหนโดยเฉพาะ หยิบไปใช้กับงานจบได้เลยสถาปัตยกรรมและโพรโทคอล
บอร์ดและซอฟต์แวร์
TESAIoT_KIT_PSE84_AI-Micropython-BentoClaw, bsps/TARGET_KIT_PSE84_AI/bsp_features.mk: เพิ่ม DPS368 · SHT40 · เรดาร์) — ภาพบอร์ดจริงยังไม่ได้ถ่ายภาพประกอบ
วิดีโอที่ตรวจแล้วว่าเปิดได้ (บันทึกใน _BUILD/MEDIA_PLAN.md)
mic อยู่ที่ ports/psoc-edge/freeze/mic.py และถูกฝังเข้าเฟิร์มแวร์ผ่าน boards/KIT_PSE84_EVAL_EPC2/manifest.py:9 (Eva) และ boards/KIT_PSE84_AI/manifest.py:11 (Dev Kit) จึง import mic ได้เลยโดยไม่ต้องคัดลอกไฟล์ขึ้นบอร์ด · มันหุ้ม machine.PDM_PCM ไว้ให้ทั้งชื่อขา ช่อง MONO_RIGHT อัตราขยาย และการหัก DC ออกจากค่าดิบ · วัดจริงบนบอร์ด 14 ส.ค. 2026 ห้องเงียบ rms 30 เปิดโทนใส่ไมค์ 19335 · การคำนวณความดังอยู่ใน PDM_PCM.stats() ซึ่งเป็นภาษา C และทิ้งเสียงที่ค้างในคิวก่อนอ่าน จึงไม่ช้ากว่าความจริง (วัดได้ 0-48 ms เทียบกับ 496-624 ms ตอนไม่ทิ้ง)sensors.init() และ sensors.scan() ถูกปฏิเสธบน Eva Kit เพราะ CM55 เป็นเจ้าของ SCB0 — modsensors.c:542 (มาโคร EVA_SCB0_REFUSE) และ :549 · ค่าทั้งหมดมาจาก sensors.snapshot() ที่ :825 ซึ่งคืนคีย์ bmi270 / capsense / pot · บน Dev Kit snapshot() คืนคีย์ชุดเดียวกัน (IMU จาก CM33 ตรง, CapSense/pot จากคอร์จอ) และ init() ทำงานได้ — แต่ไม่มีคีย์อุณหภูมิบนบอร์ดไหนsensors.bmi270.temperature() โยน OSError บน Eva Kit — modsensors.c:231 · บน Dev Kit อ่านได้ แต่เป็นอุณหภูมิของชิป IMU ไม่ใช่ของห้องsensors.sht40 (modsensors_sht40.c: temperature() humidity() temperature_humidity()) และ sensors.dps368 (modsensors_dps368.c: pressure() temperature() altitude()) — bsps/TARGET_KIT_PSE84_AI/bsp_features.mk ตั้ง BSP_HAS_SHT40=1 BSP_HAS_DPS368=1 BSP_HAS_RADAR=1 · mqtt เปิดได้เฉพาะพอร์ต 1883 ทั้งสองบอร์ด (โมดูลร่วมใน BENTO-TESAIoT-libraries)s09/09 s10/08 s11/07 s12/06 s12/09) อ่านผ่าน read_temp(): sensors.sht40 เมื่อบอร์ดมี ไม่งั้นลูกบิดแทน (0–100 % = 15–45 °C) และบอกบน console — ยังไม่ได้รันบนบอร์ดจริงทั้งสองทาง (รอรอบทดสอบกับอาจารย์)ทุกข้อจำกัดข้างบนมี path และเลขบรรทัดกำกับ ไม่ได้มาจากเอกสารฉบับใด ถ้าเจอที่ไม่ตรงกับบอร์ด บอกผู้สอนได้เลย

KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw/sim) ซึ่งรัน ipc_ui.c กับ ui_widget_mgr.c ตัวจริงเดียวกับบอร์ด บนพื้นที่วาด 792×398 (ขนาดเดียวกันบน Dev Kit) · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ examples/s12/09_tileview_swipe_only.py↗ · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ดหลายหน้าจอที่ปัดสลับกันได้ เหมาะกับแดชบอร์ดที่มีมากกว่าที่จอเดียวรับไหว
ราคา: สามช่องกิน 4 แฮนเดิล ตั้งแต่ยังไม่มีอะไรอยู่ในนั้น — ตัว Tileview หนึ่ง บวกช่องละหนึ่ง
ข้อจำกัดที่ต้องออกแบบเผื่อ
scroll_endของที่ต้องเห็นตลอดเวลา ห้ามอยู่ในช่องใดช่องหนึ่ง — สัญญาณเตือนที่อยู่ในช่องที่ผู้ใช้ไม่ได้เปิดอยู่ คือสัญญาณเตือนที่ไม่มีใครเห็น วางไว้นอก Tileview เสมอ
fit-css
VIDEO-SLOT: คลิป 60 วินาที เปิดผลงานจริงของทีมเรียง 2 ชิ้น — dashboard คาบ 8 บนจอบอร์ด แล้วสลับไปหน้าจอคอมที่ MQTT Explorer มีข้อความจากคาบ 10/11 ไหลเข้ามา จบด้วยการถอดปลั๊ก WiFi router แล้วให้กล้องจับว่าจอบอร์ดยังวาดต่อหรือค้าง
ตารางที่แล้วเรียงตาม "คาบ" ตารางนี้เรียงตาม "โมดูล" — เพราะตอนออกแบบงานจบ คำถามไม่ใช่ "คาบไหนสอนอะไร" แต่คือ "ของที่มีอยู่ในมือทั้งหมดมีอะไรบ้าง"
VIDEO-SLOT: คลิป 90 วินาที ผลงานจบของทีม — จอบอร์ดขึ้นสถานะ OK แล้วเอียงบอร์ดค้างไว้จนขึ้น WARN และ ALERT พร้อมกล้องจับหน้าจอคอมที่ MQTT Explorer มีข้อความ kind: event โผล่ขึ้นมาในวินาทีเดียวกัน จบด้วยการถอด router แล้วให้เห็นว่าจอเปลี่ยนเป็น net: offline แต่ตัวเลขยังเดิน แล้วเสียบกลับให้เห็นข้อความ kind: back