คาบ 12 — Capstone

AIoT Mini-Product ของทีมเรา: จากโจทย์จริง สู่ของที่ใช้งานได้

คาถาประจำคาบ: ของที่ใช้งานได้ ไม่ใช่ของที่ทำงานได้ตอนสาธิต

ดูของจริงก่อน — ผลงานของทีมเราเอง

dashboard คาบ 8 IMU Compass CapSense Pot telemetry คาบ 10-11 bento/team01/telemetry {"v": 17.4} {"v": 17.6} ข้อความไหลเข้าทุก 5 วินาที ถอด WiFi ออกเงียบ ๆ แล้วจอบอร์ดเป็นยังไง ค้าง · error กลางจอ หรือทำงานต่อแล้วบอกสถานะ ทั้งสองอย่างทำงานได้ทั้งคู่ และทั้งสองอย่างยังไม่ใช่ผลิตภัณฑ์

เปิดสองอย่างขึ้นมาพร้อมกัน: dashboard ที่ทีมสร้างในคาบ 8 กับ telemetry ที่ทีมส่งขึ้น broker ในคาบ 10 และ 11

ทั้งสองอย่างทำงานได้ทั้งคู่ และทั้งสองอย่างยังไม่ใช่ผลิตภัณฑ์

ลองอย่างนี้: ระหว่างที่มันรันอยู่ ให้เพื่อนถอด WiFi ออกเงียบ ๆ แล้วดูว่าเกิดอะไรขึ้นบนจอ

สามชั่วโมงนี้เราจะเปลี่ยนงานสองชิ้นนั้นให้เป็นชิ้นเดียวที่ส่งมอบได้

demo กับ product ต่างกันตรงไหน

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

ช่องขวาทั้งห้าข้อ ไม่มีข้อไหนต้องใช้ API ใหม่ที่เรายังไม่เคยเรียนเลยสักตัว

ระยะทางจาก demo ถึง product สั้นกว่าที่คิด แต่ต้องตั้งใจเดิน ไม่ใช่บังเอิญไปถึง

demo กับ product ต่างกันตรงไหน (ต่อ) — ชิ้นส่วนเดียวกับคาบ 5

ซ้าย — ภาพ: TT Zop / Wikimedia Commons — CC BY-SA 2.0 · ขวา — ภาพ: Daniel Beardsmore / Wikimedia Commons — สาธารณสมบัติ · ซ้ายคือช่องเก็บลูกบิดของกีตาร์ที่เดินสายเรียบร้อยอยู่ในตัวเครื่อง ขวาคือจอยสติกที่สร้างจาก pot สองตัวแล้วขายเป็นสินค้าจริง — ทั้งสองชิ้นใช้ชิ้นส่วนที่เราเรียนมาแล้วในคาบ 5 ความต่างจาก demo อยู่ที่สิ่งที่มองไม่เห็นในภาพ ไม่ใช่ที่ชิ้นส่วน

ทำไม · คืออะไร · ทำยังไง — แผนที่ของคาบนี้

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

ปลายทางที่จับต้องได้ — ของหนึ่งชิ้นที่วางหน้าห้องแล้วเดินครบวงเอง ตั้งแต่วัด ตัดสิน แสดง ถึงรายงานขึ้น broker และทีมอธิบายได้ว่ามันแก้ปัญหาอะไรให้ใคร รวมทั้งบอกได้ว่ามันยังทำอะไรไม่ได้

คาบ 11 เราทำให้ส่งอย่างปลอดภัยได้ · คาบนี้เราตอบคำถามที่มาก่อนหน้านั้น — จะส่งอะไร ให้ใคร และเขาจะเอาไปทำอะไรต่อ

เป้าหมายของคาบนี้

  1. เลือกโจทย์จาก ความเจ็บปวดจริงในงาน ไม่ใช่จากรายการเซนเซอร์ที่บอร์ดมี
  2. ออกแบบระบบด้วย canvas ห้าช่อง: Sense → Decide → Show → Send → Act
  3. ออกแบบ schema ข้อมูลที่เล็ก คงที่ มีหน่วย และมีตัวตนอุปกรณ์
  4. ออกแบบพฤติกรรมตอนพัง — เน็ตหลุด broker ไม่ตอบ ค่าเซนเซอร์แปลก
  5. ต่อโครง s12_capstone_starter.py จนรัน end-to-end แล้ว นำเสนอ 10 นาที

ปลายทางของวันนี้: บอร์ดวางอยู่หน้าห้อง จอบอกสถานะ ข้อความไหลขึ้น broker และทีมอธิบายได้ว่ามันแก้ปัญหาอะไรให้ใคร

วันนี้เราไม่ได้เรียน API ใหม่ เราเรียน วิธีตัดสินใจ ว่าจะเอา API ที่มีอยู่ไปทำอะไร

ปลายทางของคาบนี้ — โครงตั้งต้นที่ทีมจะต่อยอด

หน้าจอจากการรันโค้ดเฉลย รุ่นก่อนปรับหน้าจอ บน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ไม่ใช่ภาพวาดและไม่ใช่ mock-up แต่รุ่นปัจจุบันเพิ่มไม้บรรทัด ไฟสถานะสามดวง ปุ่มเปิด/ปิดไฟเตือน และกล่องยืนยันเข้าไปแล้ว ภาพชุดใหม่ยังไม่ได้ถ่าย · ชื่อ team01-eva ในภาพคือ device_id รุ่นก่อนเปลี่ยนชื่อ ปัจจุบันไฟล์ใช้ team01

โครงของหน้าจอรุ่นปัจจุบัน แบ่งเป็นสามการ์ด

  • ซ้ายบน ค่าที่วัดได้ตัวใหญ่ พร้อม ui.Bar วางทับ ui.Scale — ค่ากับพิสัยอยู่ด้วยกัน และมีป้ายบอกคุณภาพของค่าเองว่าสดหรือค้าง
  • ซ้ายล่าง ไฟสถานะสามดวง (ปกติ · เฝ้าระวัง · ผิดปกติ) ติดทีละดวง กับปุ่มรับทราบ
  • ขวา ของจริงที่สั่งได้ — ไฟเตือนหน้างาน มีปุ่มเปิดกับปุ่มปิดแยกกันคนละปุ่ม และปุ่มปิดต้องผ่านกล่องยืนยันที่บอกว่าจะเกิดอะไร

นี่คือ starter ไม่ใช่เฉลย — สิ่งที่ให้มาคือ มาตรฐานของหน้าจอ ที่ทีมต้องรักษาไว้ ส่วนเนื้อในเปลี่ยนเป็นโจทย์ของทีมได้ทั้งหมด

สิบเอ็ดคาบที่ผ่านมา เราสะสมอะไรไว้บ้าง

ภาพ: Paul McLaughlin, Rohan McAdam / Wikimedia Commons — CC BY-SA 4.0 · The Edge คือบอร์ดของเรา · HMI คือจอคาบ 8 · เส้นขึ้น cloud คือ MQTT คาบ 10-11
คาบ สิ่งที่ได้ วันนี้เอามาใช้ตรงไหน
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 กับเข็มทิศ เราจะทำอะไรกับมันดี" ผลที่ได้ ของสวยที่ไม่มีใครอยากได้ เล่าให้คนนอกฟังแล้วเขาถามว่า "แล้วไง" 1 · ใครเสียอะไรอยู่ทุกวัน เวลา เงิน ความปลอดภัย ความมั่นใจ 2 · ตอนนี้เขารู้ได้ยังไงว่ามีปัญหา เดินไปดูเอง รอลูกค้าโทรบ่น หรือรู้ตอนพังแล้ว 3 · ถ้ารู้เร็วขึ้น 30 นาที เปลี่ยนอะไรได้ ตอบไม่ได้ = โจทย์นี้ยังไม่คุ้มที่จะทำ

วิธีที่คนส่วนใหญ่เริ่ม แล้วจบไม่สวย: "บอร์ดมี IMU กับเข็มทิศ เราจะทำอะไรกับมันดี"

วิธีที่ได้ของที่มีคนใช้: เริ่มจากสามคำถามนี้ตามลำดับ

  1. ใครเสียอะไรอยู่ทุกวัน — เวลา เงิน ความปลอดภัย หรือความมั่นใจ
  2. ตอนนี้เขารู้ได้ยังไงว่ามีปัญหา — เดินไปดูเอง รอให้ลูกค้าโทรมาบ่น หรือรู้ตอนของพังไปแล้ว
  3. ถ้ารู้เร็วขึ้นสามสิบนาที จะเปลี่ยนอะไรได้บ้าง — ถ้าตอบไม่ได้ แปลว่าโจทย์นี้ยังไม่คุ้มที่จะทำ

พอตอบครบสามข้อ ค่อยถามว่า "บอร์ดของเราวัดอะไรที่พอจะบอกเรื่องนั้นได้บ้าง"

ทีมที่เดินลำดับนี้จะได้โจทย์ที่เล่าให้คนนอกฟังได้ใน 30 วินาที ทีมที่เดินย้อนกลับมักจบด้วยของสวยที่ไม่มีใครอยากได้

โจทย์ที่ดีเล่าจบก่อนที่คนฟังจะทันถามว่า "แล้วไง"

เริ่มจากปัญหา ไม่ใช่จากรายการเซนเซอร์ (ต่อ) — โจทย์ของจริงสองแบบ

ซ้าย — ภาพ: NHTSA / Wikimedia Commons — สาธารณสมบัติ · ขวา — ภาพ: Arthbkins / Wikimedia Commons — สาธารณสมบัติ · ซ้ายคือถุงลมที่กางจริงในการทดสอบชน โจทย์ที่บังคับเวลาตัดสินใจไว้ที่หลักมิลลิวินาทีและทำผิดแล้วแก้ไม่ได้ · ขวาคือเครื่องนับก้าวที่วางขายจริง โจทย์เดียว จบในกล่องเดียว ไม่มีเมนู ไม่มีการตั้งค่า — ใช้สองภาพนี้ถามทีมว่าโจทย์ของตัวเองเข้มงวดแค่ไหน และเสร็จแล้วหน้าตาควรเป็นอย่างไร

AIoT design canvas — ห้าช่องที่ต้องเติมให้ครบก่อนเขียนโค้ด

เติมจากขวาไปซ้ายก็ได้ — เริ่มที่ Act แล้วถอยกลับ มักได้ระบบที่เล็กลงและตรงกว่า Sense วัดอะไร ด้วยอะไร ถี่แค่ไหน กรองยังไง Decide เกณฑ์อะไร ตัดสินบนบอร์ด ยืนยันกี่รอบ ก่อนจึงเชื่อ Show เห็นอะไรบนจอ รู้ใน 2 วินาที สถานะเน็ต ปุ่มรับทราบ Send ส่งอะไร ไปไหน ถี่แค่ไหน schema อะไร ส่ง event Act ใครทำอะไรต่อ ภายในกี่นาที ถ้าไม่มีใครทำ ก็ไม่ต้องส่ง ช่องไหนเติมไม่ได้ แปลว่ายังไม่รู้จักโจทย์ดีพอ ไม่ใช่ยังเขียนโค้ดไม่เป็น

canvas นี้อยู่ในใบงานข้อ 4.1 กรอกให้ครบก่อนแตะคีย์บอร์ด

canvas ที่กรอกแล้วหน้าตาเป็นแบบนี้

โจทย์ตัวอย่าง · นั่งร้านชั้นสามเอียงผิดปกติแล้วไม่มีใครรู้จนเช้า ท่าตั้งต้นตอนติดตั้ง เอียงขึ้นเรื่อย ๆ Sense · ทุก 200 ms Decide · 8 / 15 องศา Show · สามระดับ Send · JSON 6 ฟิลด์ Act หัวหน้าไซต์เดินไปดู ภายใน 15 นาที จอที่หน้างานเห็น 17.4 OK WARN ALERT net: online

โจทย์ตัวอย่าง: นั่งร้านชั้นสามของไซต์ก่อสร้าง เอียงผิดปกติแล้วไม่มีใครรู้จนเช้า

ช่อง คำตอบของทีมตัวอย่าง
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 · ความสั่นมอเตอร์ปั๊ม ขนาดความเร่งรวม เกินค่าปกติ 2 เท่า ติดกัน 5 รอบ 2 · ห้องเย็นประตูเปิดค้าง Eva: ใช้ตัวแทน (ประตู) Dev Kit: SHT40 อ่านตรง ขยับแล้วไม่กลับที่เดิมใน 60 วิ 3 · การเคลื่อนย้ายทรัพย์สิน ทิศเปลี่ยนเกิน 30 องศา บอกว่า "ขยับ" ไม่บอกว่าไปไหน 4 · เครื่องจักรเดินเบา สรุปนาที idle ทุก 15 นาที 5 · การใช้งานห้องประชุม capsense = เช็กอิน ไม่ขยับ 10 นาที = ห้องว่าง 6 · การเอียงของนั่งร้าน เกิน 15 องศา ติดกัน 3 รอบ
โจทย์ วัดด้วยอะไรบนบอร์ดนี้ เกณฑ์ที่ใช้ได้จริง ข้อจำกัดที่ต้องพูดตรง ๆ
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 รอบ ต้องยึดบอร์ดให้แน่นจริง ไม่งั้นวัดการเอียงของเทปกาว

เลือกโจทย์ที่ทีมมีคนเคยเจอปัญหานั้นจริง จะเถียงกันเรื่องเกณฑ์ได้สนุกกว่ามาก

หกโจทย์ (ต่อ) — ตัวแทนของโจทย์ที่ 2 และโจทย์ที่ต้องฟังเสียง

ซ้าย — ภาพ: Bidgee / Wikimedia Commons — CC BY 3.0 · ขวา — ภาพ: Stefan Riepl (Quark48) / Wikimedia Commons — สาธารณสมบัติ · ซ้ายคือรีดสวิตช์ตัวจริงในหลอดแก้ว มีแค่แผ่นโลหะสองแผ่น ขวาคือแอนิเมชันของหน้าสัมผัสคู่นั้นตอนแม่เหล็กเข้าใกล้ (ต้นทางระบุเองว่าเป็นภาพอุดมคติ ไม่ใช่ภาพถ่ายจากของจริง) — ตัวแทน "ประตูถูกเปิดค้าง" ของโจทย์ที่ 2 ทำงานแบบนี้ อุปกรณ์ทั้งชิ้นตอบได้แค่ 1 บิต และจังหวะที่มันปิดคือเหตุผลที่สัญญาณเด้งจนต้องกรอง

หมายเหตุเรื่องเสียง: ไมโครโฟนใช้จาก 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 — สัญญาณเด้งของหน้าสัมผัสคือ "ค่าสั่นวูบเดียว" ในอีกหน้าตาหนึ่ง

เข้าใจฮาร์ดแวร์ · เมื่อบอร์ดวัดสิ่งที่เราอยากรู้ไม่ได้

ภาพซ้าย: Kolok P. et al., Sensors 25(21):6610 (2025) — CC BY 4.0 · ภาพขวา: Mika D. et al., Sensors 25(23):7371 (2025) — CC BY 4.0 · ซ้าย = สัญญาณดิบของลูกปืนปกติเทียบกับลูกปืนเสีย ซึ่งเป็นระดับที่บอร์ดเราพอจับได้ · ขวา = envelope spectrum ที่ชี้ความถี่ความผิดปกติได้ตรงตัว ซึ่งต้องใช้เครื่องมือวัดจริง ไม่ใช่ `bmi270.motion()` ที่ 5 Hz

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 ที่แอบอ้างเป็นของจริงคือการหลอกลูกค้า

ส่งอะไรขึ้นไป — schema ที่อยู่ได้นาน

{"id": "team01", "v": 17.4, "unit": "deg", "state": "ALERT", "kind": "event", "t": 812340} id มาจากบอร์ดไหน บน broker สาธารณะ ยิ่งจำเป็น v + unit ค่าที่ตัดสินแล้ว ไม่ใช่ค่าดิบ หน่วยกันตีความผิด state คำตัดสินของบอร์ด ปลายทางไม่คิดซ้ำ จะได้ไม่ขัดกัน kind event · heartbeat ack · back คนละการกระทำ t เวลาบนบอร์ด ใช้เรียงลำดับ ดูว่ามาช้าไหม

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 ไบต์เสมอ

ฝั่งรับเขียนโค้ดแกะข้อมูลครั้งเดียว ถ้าเราเปลี่ยนชื่อฟิลด์ทีหลัง เขาต้องแก้ทั้งระบบ

ส่งเหตุการณ์ ไม่ใช่สตรีมดิบ

ส่งทุกค่า ทุก 200 ms 432,000 ข้อความต่อวัน 39 MB ต่อบอร์ด · 100 บอร์ด = 3.9 GB ต่อวัน ส่งตอนสถานะเปลี่ยน + heartbeat ทุก 30 วินาที event จริง 2,883 ข้อความต่อวัน ลดลงร้อยกว่าเท่า ปลายทางอ่านง่ายกว่าเดิม

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

ลองคิดเลขให้เห็นภาพ อ่านค่าทุก 200 ms แล้วส่งทุกค่า

  • 5 ครั้งต่อวินาที × 86,400 วินาที = 432,000 ข้อความต่อวัน ต่อหนึ่งบอร์ด
  • ข้อความละ ~90 ไบต์ = ราว 39 MB ต่อวัน ต่อบอร์ด · มี 100 บอร์ดคือ 3.9 GB ต่อวัน
  • ในจำนวนนั้น เหตุการณ์ที่มีคนต้องทำอะไรจริง ๆ อาจมีวันละ 3 ครั้ง

แบบส่งเหตุการณ์: ส่งตอนสถานะเปลี่ยน + heartbeat ทุก 30 วินาที = 2,883 ข้อความต่อวัน ลดลงร้อยกว่าเท่า และปลายทางอ่านง่ายกว่าเดิม

ทำไมยังต้องมี heartbeat: ถ้าเงียบอย่างเดียว ปลายทางแยกไม่ออกระหว่าง "ทุกอย่างปกติ" กับ "บอร์ดตายไปแล้วเมื่อวาน"

ดูเพิ่ม (6 นาที): MQTT Essentials Part 6 — MQTT Topic Best Practices — HiveMQ — 5:50 — การออกแบบลำดับชั้น topic และ wildcard + # ที่ฝั่งรับใช้ query

ความเงียบไม่ใช่ข่าวดี จนกว่าเราจะออกแบบให้ความเงียบมีความหมาย

เกร็ด: MQTT เกิดมาเพื่อสัญญาณที่แย่

ภาพ: Chine3me / Wikimedia Commons — CC0 1.0 · ผู้ส่งกับผู้รับไม่เคยรู้จักกัน broker เป็นคนกลาง — โครงสร้างนี้เองที่ทำให้อุปกรณ์หลุดแล้วระบบไม่ล้มทั้งเส้น

ดูเพิ่ม (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 กำลังใช้มันผิดวิธี และจะเจอปัญหาเดียวกับที่ท่อน้ำมันเจอ

ออกแบบตอนพัง — สิ่งที่แยก product ออกจาก demo

connected ส่ง event + heartbeat จอขึ้น net: online retrying นัดต่อใหม่ทุก 10 วินาที ไม่ต่อรัว ๆ ในลูป offline นับที่ส่งไม่สำเร็จไว้ ทิ้ง หรือ เก็บไว้ส่งทีหลัง เน็ตหลุด ต่อไม่ติด เน็ตกลับมา ต่อเอง แล้วส่ง kind: back บอกฝั่งรับทันที ไม่ว่าอยู่สถานะไหน ลูปยังเดินครบทุกรอบ จอจึงไม่มีวันค้าง ทั้งสองคำตอบเรื่อง "ทิ้ง หรือ เก็บ" ถูกได้ ที่ผิดคือไม่เคยตัดสินใจ

เน็ตหลุดกลางงานไม่ใช่กรณีพิเศษ มันคือสภาพปกติของอุปกรณ์ที่ติดตั้งจริง

สถานการณ์ demo ทำ product ต้องทำ
WiFi หลุด โปรแกรมตาย หรือค้างรอ จอวาดต่อ ขึ้นคำว่า offline แล้วนัดลองใหม่ทุก 10 วินาที
broker ไม่ตอบ publish เงียบ ไม่มีใครรู้ นับจำนวนครั้งที่ส่งไม่สำเร็จ แล้วโชว์บนจอ
เน็ตกลับมา ต้องรีเซ็ตบอร์ดเอง ต่อเอง แล้วส่งข้อความบอกว่ากลับมาแล้ว
ค่าเซนเซอร์กระโดดวูบเดียว เตือนทันที คนวิ่งมาดูแล้วไม่เจออะไร ต้องเกินเกณฑ์ติดกันหลายรอบจึงเปลี่ยนสถานะ
เตือนแล้วไม่มีใครอยู่หน้าจอ ข้อความแวบเดียวแล้วหาย ค้างสถานะไว้จนมีคนกดรับทราบ

การตัดสินใจที่ต้องเลือกให้ชัดตั้งแต่วันนี้: ตอนออฟไลน์ ทีมจะ ทิ้ง ข้อมูลหรือ เก็บไว้ส่งทีหลัง

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

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

ออกแบบตอนพัง (ต่อ) — เครื่องยังมีชีวิตไหม คนเดินผ่านรู้ได้จากอะไร

ภาพ: Morn / Wikimedia Commons — CC0 1.0 — แผง LED ของซูเปอร์คอมพิวเตอร์ Thinking Machines CM-5 ที่วิ่งตามภาระงานจริง ไม่ใช่ลูปตกแต่ง: จังหวะไฟคือหลักฐานว่าเครื่องยังมีชีวิตและยังทำงานอยู่ ของที่ product มีแล้วแต่ demo มักไม่มี · ถามทีมตรง ๆ ว่า ถ้าเครื่องของเราค้าง คนที่เดินผ่านจะรู้ได้จากอะไร

เรื่องที่เราให้ 70% น้อง ๆ เขียน 30%

70% — มีให้แล้ว เฟิร์มแวร์อ่านเซนเซอร์และวาดจอ · โมดูล sensors dsp ui wifi mqtt โครง s12_capstone_starter.py ที่รันครบวงจรตั้งแต่ยังไม่แก้อะไร 30% — งานของทีม เลือกโจทย์ · ตั้งเกณฑ์ · ออกแบบจอ schema · ออฟไลน์ · เล่าให้เข้าใจ

สิ่งที่มีให้แล้ว (70%) — เฟิร์มแวร์อ่านเซนเซอร์และวาดจอ, โมดูล sensors/dsp/ui/wifi/mqtt, และโครง s12_capstone_starter.py ที่รันครบวงจรได้ตั้งแต่ยังไม่แก้อะไร

งานของทีม (30%) — เลือกโจทย์ · เลือกว่าวัดอะไร · ตั้งเกณฑ์ · ออกแบบสิ่งที่คนหน้างานเห็น · ออกแบบ schema · ตัดสินใจเรื่องออฟไลน์ · เล่าให้คนอื่นเข้าใจใน 10 นาที

30% ของวันนี้ไม่ได้วัดกันที่จำนวนบรรทัด มันวัดกันที่ คุณภาพของการตัดสินใจ ทุกอย่างข้างบนคือการตัดสินใจ ไม่ใช่การพิมพ์

คาบก่อน ๆ เราวัดกันว่า "ทำให้มันทำงานได้ไหม" วันนี้เราวัดว่า "ทำไมถึงทำแบบนี้"

แกะโครงเริ่มต้น — ท่าที่ 1 Sense

motion() ax ay az gx gy gz dsp.tilt() roll, pitch roll มาก่อนเสมอ dsp.EMA(0.2) กันค่ากระโดด ครั้งเดียวจนเตือนผิด ค่าเดียว 17.4 หกแกนเข้า ค่าเดียวออก — สามช่องที่เหลือของ canvas ทำงานกับค่าเดียวนั้นทั้งหมด ทีมความสั่นเปลี่ยนเป็นขนาดความเร่งรวม · ทีมการเคลื่อนย้ายใช้ bmm350.heading() — โครงที่เหลือไม่ต้องแตะ
# บน 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 ในใบงานยังกรอกไม่เสร็จ

ท่าที่ 1 ต่อ — โครงเดียว รันได้ทั้งสองบอร์ดโดยไม่ต้องแก้

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 ซึ่งเป็นดวงแดง

โค้ดที่รันได้ทั้งสองบอร์ดไม่ได้เกิดจากการ "ไม่พูดถึงบอร์ด" แต่เกิดจากการถามบอร์ดว่ามันมีอะไร แล้วเลือกด้วยชื่อ

แกะโครงเริ่มต้น — ท่าที่ 2 Decide

ยืนยันติดกัน 3 รอบก่อนเปลี่ยนสถานะ — 3 x 200 ms = หกในสิบวินาที LIMIT วูบเดียว ไม่เปลี่ยนสถานะ เกินติดกัน 3 รอบ = ALERT

ภาพ: Psenda38 / Wikimedia Commons — CC0 1.0 · การตัดสินอยู่บนบอร์ด ไม่ได้อยู่ที่ปลายทาง — เน็ตหลุดแล้วต้องยังตัดสินได้ และไม่ต้องจ่ายค่าเน็ตเพื่อรอคำตอบที่ช้ากว่า
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) โครงเริ่มต้นเชื่อทันทีที่เห็นค่าเกินครั้งเดียว ซึ่งพอสำหรับให้ไฟล์รันได้ แต่จะโทรตามช่างเพราะรถบรรทุกวิ่งผ่าน

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

แกะโครงเริ่มต้น — ท่าที่ 3 Show

ค่าที่วัดได้ เทียบกับเกณฑ์ 17.9 deg ค่าปกติ 0 15 30 45 สถานะที่ตัดสินแล้ว ปกติ เฝ้าระวัง ผิดปกติ ผิดปกติ ต้องมีคนไปดู รับทราบ ไฟเตือนหน้างาน ไฟติดอยู่ เปิดไฟเตือน ปิดไฟเตือน สั่งปิดต้องยืนยันก่อน online · ส่งแล้ว 12 ใบ ค่า + พิสัย Bar ทับ Scale สถานะเป็นไฟ ไม่ใช่ตัวอักษรสี เปิด/ปิด คนละปุ่ม 31 จาก 64 ยังเหลือให้ทีม ถ้าจอนี้ติดอยู่หน้าเครื่องจักรจริง คนเดินผ่านจะเข้าใจใน 2 วินาทีไหม
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 ถูกยก ไม่ใช่ค้างเลขเดิมไว้เฉย ๆ

ท่าที่ 3 ต่อ — ไม้บรรทัดไม่ขยับ ไฟหรี่ไม่หาย และจอต้องบอกความจริงเรื่องเน็ต

ui.Scale ไม่รับ .value() มันคือไม้บรรทัด ตัวที่ขยับคือ ui.Bar ที่วางทับ (แบบวงกลมมีเข็มจริง — คาบ 6) · ui.Led สั่ง .value(0) แล้ว หรี่ ไม่ใช่หาย ซึ่งตั้งใจ เพราะไฟที่หายไปตอนดับ ทำให้คนดูแยกไม่ออกว่าดับหรือจอเสีย

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

บรรทัด lbl_net มีอยู่เพราะจอต้องบอกความจริงเรื่องการเชื่อมต่อด้วย ไม่ใช่โชว์แต่ตัวเลขสวย ๆ ราวกับทุกอย่างปกติทั้งที่ส่งอะไรไม่ออกมาสิบนาทีแล้ว

ภาพ: smial / Wikimedia Commons — CC0 1.0 — จังหวะกะพริบแบบหัวใจเต้น คือช่อง Show ที่ถูกที่สุดเท่าที่มี ใช้ยืนยันว่าลูปยังเดินอยู่แม้จอจะยังไม่มีข้อมูลอะไรใหม่ให้แสดง · ทีมที่ยังไม่มีอะไรจะโชว์ ให้เริ่มจากอันนี้ก่อน

ถามตัวเองว่า "ถ้าจอนี้ติดอยู่หน้าเครื่องจักรจริง คนเดินผ่านจะเข้าใจใน 2 วินาทีไหม"

ท่าที่ 3 ต่อ — คำสั่งที่ทำให้ของจริงขยับ ต้องยืนยันก่อน

# สร้างพร้อมหน้าจอแล้วซ่อนไว้ ไม่ใช่สร้างตอนกด - แฮนเดิลมีจำกัด และการสร้างของ
# ตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · กล่องวางกลางจอ
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

  1. ปุ่มในตัว MsgBox เองยังไม่ส่งเหตุการณ์กลับมาให้ Python เห็น เฟิร์มแวร์ผูก callback ไว้กับ ui.Button เท่านั้น ถ้าวางปุ่มของกล่องไว้แล้วรอให้คนกด จะได้ปุ่มตายบนจอ และคนกดจะสรุปว่าเครื่องแฮงก์ — ใช้ ui.Button จริงสองตัวเป็นคำตอบแทน
  2. ข้อความของ MsgBox เดินทางไปกับ CREATE ซึ่งพาได้ 95 ไบต์ ภาษาไทยตัวละ 3 ไบต์ แปลว่าหัวเรื่องบวกเนื้อความรวมกันได้ราว 31 ตัวอักษร ยาวกว่านั้นถูกตัดเงียบ ๆ ไม่มี error ให้จับ มีแต่ประโยคที่ขาดครึ่งบนจอ

คำยืนยันต้องบอก สิ่งที่จะเกิด ไม่ใช่ถามว่า "ยืนยันไหม" — เทียบสองประโยคนี้ตอนตีสามที่หน้างาน: "ยืนยันหรือไม่" กับ "ไฟหน้างานจะดับทันที" · ทีมที่ทำหน้าจอสั่งงานได้ ต้องตอบให้ได้ว่า "ถ้ากดผิดจะเกิดอะไร และย้อนกลับได้ไหม"

ท่าที่ 3 ต่อ — ตัวเลขที่คนต้องอ่าน เขียนใหม่ไม่เกินวินาทีละครั้ง

    sec = now // 1000              # ประตูเดียว: วินาทีเปลี่ยนหรือยัง
    if sec == last_sec:
        return                     # แถบกับไฟขยับไปแล้วข้างบน ส่วนตัวเลขรอรอบหน้า
    last_sec = sec
    lbl_value.text("{:.1f} {}".format(value, UNIT))

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

สามจังหวะในฟังก์ชัน show() เดียวกัน: ทุกรอบ — แถบค่าและไฟสามดวง · ตอนเปลี่ยน — ป้ายสถานะและป้าย "ค้างอยู่" · วินาทีละครั้ง — ตัวเลขค่า ป้ายคุณภาพ สถานะเน็ต และตัวนับใบที่ส่ง

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

แกะโครงเริ่มต้น — ท่าที่ 4 Send และท่าที่ 5 กันเน็ตหลุด

นัดเวลาลองใหม่ ไม่ใช่พยายามทุกรอบลูป ต่อรัว ๆ ทุกรอบลูป ลูปหน่วง จอกระตุก และก็ยังไม่ติดอยู่ดี RETRY_MS = 10 วินาที ลูปเดินครบทุกรอบ จอไม่ค้าง ต่อติดแล้วส่ง kind: back ทันที client_id = DEVICE_ID ต้องไม่ซ้ำ — สองบอร์ดใช้ id เดียวกันจะเตะกันหลุดสลับไปมาเป็นลูป
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 ตัวตนอุปกรณ์รายทีมก่อน จึงไม่อยู่ในโครงเริ่มต้น

ข้อมูลไหลไปทางไหน — ทั้งวงจรในภาพเดียว

BMI270 ทุก 200 ms EMA + tilt เหลือค่าเดียว decide() OK / ALERT จอ HMI เห็นทันที เสมอ MQTT 1883 เฉพาะเหตุการณ์ คนที่รับผิดชอบ ไปทำอะไรต่อ เน็ตหลุด: นับที่พลาดไว้ นัดต่อใหม่ทุก 10 วินาที แต่เส้นทางไปจอไม่ขาดตอน เส้นบนไม่พึ่งเน็ต เส้นล่างพึ่ง — ออกแบบให้ของสำคัญอยู่บนเส้นบน

ดูเพิ่ม (6 นาที): MQTT Essentials Part 7 — Quality of Service — HiveMQ — 5:41 — QoS 0/1/2 คือคำตอบระดับโพรโทคอลของคำถามเดียวกันนี้ และราคาที่ต้องจ่ายของแต่ละระดับ

จอกับ broker ต้องเป็นเส้นทางที่แยกกันได้ ถ้าฝั่งหนึ่งล้ม อีกฝั่งต้องไม่ล้มตาม

วิธีรันบนบอร์ด

1-2 · เตรียม เปิด Playground ค้างไว้ เปิดไฟล์ starter ใน IDE 3-4 · รันโครงเปล่า แก้ CONFIG ให้เป็นของทีม ยังไม่ต้องแก้ตรรกะอะไร 5-6 · ดูปลายทาง MQTT Explorer subscribe เอียงเกิน 15 องศาค้างไว้ 7 · ทดสอบการพัง ปิด WiFi หรือถอด router จอวาดต่อ ขึ้น offline 8 · ค่อยแทนที่ส่วนที่เขียนว่า "ทีมเขียนเอง" ทีละจุด แล้วรันดูทุกครั้ง wifi.connect() บล็อกได้นาน อย่าเพิ่งรีบกดรันซ้ำ รอให้มันตอบก่อน
  1. บนจอบอร์ด แตะการ์ด BENTO Playground แล้วค้างหน้านี้ไว้
  2. เปิด practise_codes/s12_capstone_starter.py↗ ใน BENTO IDE
  3. แก้บล็อก CONFIG ให้เป็นของทีม: DEVICE_ID, WIFI_SSID, WIFI_PASS, TOPIC (ใช้ bento/teamNN/... ของทีมเท่านั้น กันชนกับทีมอื่นบน broker สาธารณะ)
  4. กด Program to Device แล้วดูจอบอร์ด — ยังไม่ต้องแก้ตรรกะอะไร มันต้องรันได้แล้วตั้งแต่ตอนนี้
  5. เปิด MQTT Explorer บนคอม แล้ว subscribe bento/teamNN/# เพื่อดูข้อความของทีม
  6. เอียงบอร์ดเกิน 15 องศาค้างไว้ ดูว่าจอเปลี่ยนสถานะและมีข้อความ kind: event ขึ้น broker
  7. ทดสอบการพัง: ปิด WiFi ที่เราต่อ (หรือถอด router) แล้วสังเกตว่าจอยังวาดต่อและขึ้น offline
  8. ค่อยเริ่มแทนที่ส่วนที่เขียนว่า "ทีมเขียนเอง" ทีละจุด แล้วรันดูทุกครั้ง

ถ้าเชื่อมต่อไม่ผ่าน wifi.connect() อาจบล็อกอยู่นาน อย่าเพิ่งรีบกดรันซ้ำ รอให้มันตอบก่อน

รันโครงให้ผ่านตั้งแต่ยังไม่แก้อะไร คือการพิสูจน์ว่าพื้นดีก่อนขึ้นบ้าน

ตัวอย่างชุดคาบ 12 — สามไฟล์แรกคือชุดที่ทำให้ demo วันนำเสนอไม่ล้ม

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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 ยาว ๆ ที่ยึดลูปไว้

ตัวอย่างชุดคาบ 12 (ต่อ) — อ่านเสริม และโจทย์ที่แหล่งค่าเป็นเสียง

อ่านเสริมนอกเวลา — เรื่องนี้เป็นไฟล์ของคาบ 7 ไม่ใช่ชิ้นส่วนของโครงวันนี้ และไม่อยู่ในเกณฑ์ผ่านของคาบ 12: examples/s07/01_imu_vibration_monitor.py↗ เป็นโจทย์ capstone ที่ทำจบได้จริงในหนึ่งคาบ — เฝ้าการสั่นเทียบเส้นฐานที่วัดเอง แล้วรายงานเป็นกี่เท่าของเส้นฐาน · เปิดตอนเลือกโจทย์ยังไม่ลงตัว

โจทย์ที่ต้องฟังเสียงทำได้แล้ว โมดูล mic เปิดไมโครโฟนจาก Python ได้ทั้งบนบอร์ดและในอีมูเลเตอร์ ทีมที่อยากให้แหล่งค่าเป็นเสียงจึงเอา mic.level() หรือ mic.peak() ไปเสียบช่อง Sense ของ canvas ได้ตรง ๆ · กฎ confirm-N ในไฟล์ที่ 2 ข้างบนใช้ครอบค่าจากไมค์ได้เหมือนกับค่าจากเซนเซอร์ตัวอื่นทุกประการ ตัวอย่างไมค์สองไฟล์อยู่ที่คาบ 8

MVP checkpoint — ผ่านคาบนี้เมื่อ

demo end-to-end + นำเสนอ 10 นาที (ปัญหา→สถาปัตยกรรม→demo→ข้อจำกัด)

แปลเป็นสิ่งที่ตรวจได้จริง:

  • [ ] canvas ห้าช่องในใบงานกรอกครบ และเล่าโจทย์ได้ใน 30 วินาที
  • [ ] บอร์ดอ่านเซนเซอร์ → ตัดสิน → แสดงบนจอ ครบวงจร รันต่อเนื่องได้อย่างน้อย 10 นาที
  • [ ] มีข้อความขึ้น broker ที่ topic ของทีม และเปิดให้ผู้สอนดูใน MQTT Explorer ได้
  • [ ] payload เป็น JSON ตาม schema ที่ทีมเขียนไว้ในใบงาน มีหน่วยและ device id
  • [ ] ตัดเน็ตแล้วจอยังทำงาน ขึ้นสถานะ offline และต่อกลับเองเมื่อเน็ตกลับมา
  • [ ] หน้าจอผ่านสี่ข้อของแผงควบคุม — ค่ามาพร้อมพิสัย · สถานะเป็นไฟไม่ใช่ตัวอักษรสี · ปุ่มเปิดกับปุ่มปิดแยกกัน · คำสั่งที่ทำให้ของจริงขยับมีกล่องยืนยันที่บอกว่าจะเกิดอะไร
  • [ ] นำเสนอ 10 นาทีครบสี่ช่วง และตอบคำถามข้อจำกัดของระบบตัวเองได้อย่างน้อย 2 ข้อ

ข้อที่ห้าถึงเจ็ดคือข้อที่แยกทีมที่ทำ 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 ให้จับสักตัว มีแต่จอที่ผิด

ลงมือทำ — ห้าจุดที่เขียนว่า "ทีมเขียนเอง"

read_value() ปริมาณของทีม เกณฑ์ใน CONFIG ทดสอบด้วยมือจริง on_state_change นับ จดเวลา เปลี่ยนจอ widget + schema งบรวมไม่เกิน 64 ออฟไลน์: ทิ้งหรือเก็บ ทดสอบตอนเน็ตหลุด ลำดับที่แนะนำ — รันโครงเปล่าให้ผ่านก่อนเสมอ ห้ามข้ามขั้นตั้งเกณฑ์ — ทีมที่ตั้งจากการเดาแล้วไม่เคยทดสอบ จะไปเจอตอนนำเสนอหน้าห้องว่ามันไม่เตือน เกณฑ์ที่ยังไม่เคยถูกทดสอบด้วยมือ ไม่ใช่เกณฑ์ มันคือความหวัง
    # ทีมเขียนเอง: เปลี่ยนบรรทัดล่างเป็นปริมาณที่โจทย์ของทีมสนใจจริง ๆ
    return smooth.update(abs(roll))

def on_state_change(old, new, value):
    pass  # ทีมเขียนเอง: ตอนสถานะเปลี่ยนให้เกิดอะไร

    # ทีมเขียนเอง: เพิ่ม widget ของทีม (งบรวมทั้งจอไม่เกิน 64 ตัว ตอนนี้ใช้ไป 31)

    # ทีมเขียนเอง: เพิ่มหรือตัดฟิลด์ให้ตรงกับตาราง schema ที่ทีมออกแบบไว้ในใบงาน

                pass  # ทีมเขียนเอง: ออฟไลน์แล้วจะ "ทิ้ง" หรือ "เก็บไว้ส่งทีหลัง"

ลำดับที่แนะนำ: หนึ่ง รันโครงเปล่าให้ผ่านก่อน สอง เปลี่ยน read_value() เป็นของทีม สาม ตั้งเกณฑ์ใน CONFIG แล้วทดสอบด้วยมือจริงว่ามันเตือนตอนที่ควรเตือน สี่ แต่งหน้าจอ ห้า ปรับ schema หก ทดสอบตอนเน็ตหลุด

ห้ามข้ามข้อสาม: ทีมที่ตั้งเกณฑ์จากการเดาแล้วไม่เคยทดสอบ จะไปเจอตอนนำเสนอหน้าห้องว่ามันไม่เตือน

เกณฑ์ที่ยังไม่เคยถูกทดสอบด้วยมือ ไม่ใช่เกณฑ์ มันคือความหวัง

นำเสนอ 10 นาที — เรียงแบบนี้แล้วคนฟังเข้าใจ

run-sheet สิบนาที — ซ้อมจับเวลาอย่างน้อยหนึ่งรอบ 1 · ปัญหา 2 นาที 2 · สถาปัตยกรรม 2 นาที 3 · demo สด + ตัดเน็ตให้ดู 4 นาที — ช่วงที่ยาวที่สุด 4 · ข้อจำกัด 2 นาที พลาดบ่อย: อวดบอร์ดก่อนเล่าปัญหา พลาดบ่อย: ไล่อธิบายโค้ดทีละบรรทัด พลาดบ่อย: ฉายวิดีโอที่อัดไว้แทนของจริง พลาดบ่อย: บอกว่าไม่มีข้อจำกัด
ช่วง เวลา สิ่งที่ต้องพูด ที่คนมักพลาด
1 · ปัญหา 2 นาที ใครเดือดร้อน เดือดร้อนยังไง วันนี้เขาแก้ยังไงอยู่ เริ่มด้วยการอวดบอร์ดแทนการเล่าปัญหา
2 · สถาปัตยกรรม 2 นาที canvas ห้าช่อง + schema ที่ส่งจริง ไล่อธิบายโค้ดทีละบรรทัด
3 · demo สด 4 นาที ทำให้มันเตือนต่อหน้าคนดู + โชว์ข้อความบน broker + ตัดเน็ตให้ดู ฉายวิดีโอที่อัดไว้แทนของจริง
4 · ข้อจำกัดและก้าวต่อไป 2 นาที สิ่งที่ระบบนี้ยังทำไม่ได้ และถ้ามีเวลาอีกสองสัปดาห์จะทำอะไร บอกว่า "ไม่มีข้อจำกัด"

เตรียม แผนสำรอง ไว้เสมอ: WiFi ห้องเรียนล่มตอนนำเสนอเป็นเรื่องที่เกิดขึ้นจริง ทีมที่ออกแบบ offline mode ไว้ดี จะเปลี่ยนอุบัติเหตุนั้นให้กลายเป็นจุดขายของตัวเองได้ทันที

ซ้อมจับเวลาอย่างน้อยหนึ่งรอบ สิบนาทีสั้นกว่าที่ทุกทีมคิดเสมอ

ช่วงที่ 4 คือช่วงที่กรรมการดูว่าทีมเข้าใจงานตัวเองจริงไหม ห้ามข้าม

เกณฑ์การนำเสนอ — ใช้ตรวจตัวเองก่อนขึ้นพูด

เจ็ดหัวข้อ ข้อละ 2 คะแนน — ติ๊กให้ครบก่อนขึ้นพูดจริง ปัญหาชัด — วัดความเดือดร้อนเป็นเวลาหรือเงิน สถาปัตยกรรมอ่านออก — คนฟังวาด canvas ตาม schema สมเหตุผล — ทุกฟิลด์อธิบายได้ มีหน่วย demo สดผ่าน — เตือนได้จริงต่อหน้าคนดู ทดสอบการพัง — ตัดเน็ตให้ดู รู้ข้อจำกัดตัวเอง — อย่างน้อย 2 ข้อ ตรงเวลา — จบใน 10 นาที ครบสี่ช่วง การซ้อมที่ได้ผลที่สุด ให้เพื่อนอีกทีมฟัง แล้วให้เขาเล่ากลับมา สองข้อขวาบนคือข้อที่แยกทีมที่ทำ product ออกจากทีมที่ทำ demo
หัวข้อ ผ่าน (2 คะแนน) ยังไม่ผ่าน (0-1)
ปัญหาชัด บอกได้ว่าใครเดือดร้อน วัดความเดือดร้อนเป็นเวลาหรือเงินได้ เล่าแต่ว่าทำอะไร ไม่บอกว่าเพื่อใคร
สถาปัตยกรรมอ่านออก คนฟังวาด canvas ห้าช่องตามได้ กระโดดเข้าโค้ดทันที
schema สมเหตุผล ทุกฟิลด์อธิบายได้ว่าปลายทางใช้ทำอะไร มีหน่วยครบ ส่งค่าดิบทุกอย่างเพราะ "เผื่อไว้"
demo สดผ่าน ระบบเตือนได้จริงต่อหน้าคนดู และเห็นข้อความขึ้น broker เปิดวิดีโอที่อัดไว้
ทดสอบการพัง ตัดเน็ตให้ดู แล้วอธิบายพฤติกรรมที่ออกแบบไว้ ไม่เคยลอง
รู้ข้อจำกัดตัวเอง บอกได้อย่างน้อย 2 ข้อ พร้อมเหตุผลเชิงเทคนิค ตอบว่าใช้งานได้ทุกกรณี
ตรงเวลา จบใน 10 นาที ครบทั้งสี่ช่วง เกินเวลา หรือรีบข้ามช่วงที่ 4

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

ตารางนี้อยู่ในใบงานด้วย ติ๊กให้ครบก่อนขึ้นพูดจริง

ตัวอย่างที่ทำเสร็จ — ส่วนที่หนึ่ง: Sense ที่เชื่อถือได้

ไม่หักค่าตั้งต้น เกณฑ์ 15 ติดเอียง 6 องศาตั้งแต่วันแรก เตือนตลอดไปโดยไม่มีอะไรผิด ไม่มีใครติดตั้งอะไรได้ระนาบ 0 องศาเป๊ะ จำท่าตั้งต้นสองวินาทีแรก base_roll · base_pitch วัดส่วนต่างจากท่าติดตั้ง รวม roll กับ pitch แบบพีทาโกรัส เอียงไปทางไหนก็นับ ไม่รอดสายตา

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 ตัวเดียว นั่งร้านที่เอียงไปข้างหน้าจะรอดสายตาไปเฉย ๆ

ค่าที่วัดเทียบกับตัวเองตอนติดตั้ง มีความหมายกว่าค่าที่วัดเทียบกับแรงโน้มถ่วงเสมอ

ตัวอย่างที่ทำเสร็จ — ส่วนที่สอง: Decide ที่ไม่หลอน

CONFIRM_N = 3 กันการสั่นวูบเดียวตอนมีคนเดินชน 3 x 200 ms = หกในสิบวินาที latched ALERT ค้างอยู่ รับทราบ เหตุการณ์ที่ไม่มีใครเห็น = ไม่เคยเกิด ALERT_GAP_MS กันการเตือนรัวตอนค่าแกว่งรอบเกณฑ์ เตือน 30 ครั้ง/นาที = ถูกปิดเสียง
    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 ตัดสินจบที่เดียว 3 · Show จอไม่พึ่งเน็ต 4 · Send พึ่งสิ่งที่คุมไม่ได้ 5 · กันเน็ตหลุด ทีมส่วนใหญ่ไม่มีเวลา เส้นที่ยังทำงานเมื่อเน็ตหาย เส้นที่พึ่งเน็ต — ล้มแล้วไม่ลากคนอื่นล้ม เขียน Show ก่อน Send เสมอ ไม่งั้นจะเผลอเขียนโค้ดที่จออัปเดตก็ต่อเมื่อส่งสำเร็จ

ท่า 1 Sense มาก่อน เพราะถ้าค่าที่อ่านยังเชื่อไม่ได้ ทุกอย่างที่สร้างทับบนมันคือการตกแต่งความผิดพลาด

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

ท่า 3 Show มาก่อน Send เพราะจอไม่พึ่งเน็ต ถ้าเรียงกลับกัน ทีมมักเผลอเขียนโค้ดที่จอจะอัปเดตก็ต่อเมื่อส่งสำเร็จ ซึ่งเป็นบั๊กที่หาเจอยากมากตอนสาย WiFi ดี

ท่า 4 Send มาที่สี่ เพราะมันคือส่วนที่พึ่งพาสิ่งที่เราควบคุมไม่ได้มากที่สุด

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

เรียงตามความน่าเชื่อถือจากมากไปน้อย: เซนเซอร์ → ตรรกะ → จอ → เน็ต

โครงของงานจบ — สี่ขั้นที่ทุกผลิตภัณฑ์ AIoT มีเหมือนกัน

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 เลย ถ้าแก้ที่เดียวจบ แปลว่าทีมแยกส่วนถูกต้องแล้ว และเปลี่ยนโจทย์ได้โดยไม่ต้องเขียนใหม่ทั้งไฟล์

โครงนี้ใช้กับงานจบของทุกทีมได้ — เปลี่ยนแค่ว่า วัดอะไร · ตัดสินด้วยกฎอะไร · สั่งอะไร

เชื่อมจุดให้เห็นภาพ — สิบสองคาบมาจบตรงนี้

คาบ 1-2 เล่นของที่มีอยู่ รู้ว่าปลายทางหน้าตายังไง คาบ 3-5 สั่งฮาร์ดแวร์เอง ไฟ ปุ่ม จอสัมผัส อนาล็อก คาบ 6-11 อ่านเซนเซอร์ วาดแดชบอร์ด ต่อเน็ต ส่งขึ้นแพลตฟอร์ม วันนี้ · คาบ 12 ตัดสินใจว่าจะทำอะไร แล้วส่งมอบให้มีคนใช้ สิบเอ็ดคาบสอนวิธีทำ วันนี้สอนวิธีเลือกว่าจะทำอะไร และวิธีบอกคนอื่นว่าทำไม

สิ่งที่ติดตัวไปแม้เปลี่ยนบอร์ดเปลี่ยนภาษา: การเริ่มจากปัญหาไม่ใช่จากเทคโนโลยี · การแยกการตัดสินใจออกจากการแสดงผลและการส่งข้อมูล · การออกแบบพฤติกรรมตอนพังตั้งแต่ต้น ไม่ใช่ตอนเจอปัญหา · การสื่อสารคุณค่าให้คนที่ไม่ได้เขียนโค้ดเข้าใจ

ดูเพิ่ม (25 นาที · ดูเป็นการบ้าน): Build Real-Time IoT Dashboard: Node-RED + InfluxDB + Grafana + MQTT — IoT Frontier — 24:54 — ปลายทางของเส้นทางที่ 4 หน้าตาเป็นอย่างไรเมื่อมีอุปกรณ์หลายตัวและต้องเก็บย้อนหลัง

เครื่องมือจะเปลี่ยนทุกสามปี วิธีคิดสี่ข้อข้างบนอยู่กับเราได้ทั้งอาชีพ

ใช้จริงที่ไหน — สี่มุมที่ของแบบนี้ทำงานอยู่

โรงงาน · เฝ้าความสั่นเครื่องจักร เกณฑ์บนบอร์ด แจ้งเฉพาะตอนผิดปกติ ค่าติดตั้งจริงมาจากการวัดเครื่องนั้น ๆ ก่อนหนึ่งสัปดาห์ โครงเดียวกับ starter เปลี่ยนแค่ read_value() ก่อสร้าง · เตือนการเอียงของโครงสร้าง นั่งร้าน แบบหล่อ ชั้นวางสูง ตู้คอนเทนเนอร์ ต้องหักค่าตั้งต้นตอนติดตั้ง และค้างสถานะรอรับทราบ คือโจทย์ที่เฉลยของคาบนี้ทำจนจบ คลังสินค้า · ติดตามทรัพย์สิน ของถูกยก ถูกเปิด หรือถูกย้ายนอกเวลางาน IMU + เข็มทิศ บอกได้ว่า "ขยับ" แต่ไม่บอกว่าไปไหน ข้อจำกัดนี้ต้องพูดตอนนำเสนอ ไม่ใช่ซ่อน อาคาร · การใช้งานพื้นที่ ห้องประชุมถูกจองแต่ไม่มีคนใช้จริง นับเวลาใช้งานจริงต่อสัปดาห์ ใช้ต่อรองเรื่องพื้นที่ได้ ข้อมูลจริงหนึ่งเดือน ชนะการเดาสิบปี

ทั้งสี่มุมใช้โครงโปรแกรมเดียวกับที่ทีมกำลังจะเขียนวันนี้ ต่างกันที่โจทย์และเกณฑ์เท่านั้น

ต่อยอด — หลังจบคอร์สนี้เดินต่อทางไหนได้บ้าง

เส้นทาง 1 · ยกระดับความปลอดภัย mqtt 1883 ไปเป็น tesaiot.connect() TLS 8884 อธิบายได้ว่า serverTLS ปกป้องอะไร ไม่ปกป้องอะไร เส้นทาง 2 · ให้ช่างตั้งค่าเองได้หน้างาน wifi.softap() เปิดวงของบอร์ดเอง ไม่ต้องแก้โค้ด ชื่อวงกับรหัสไม่ควรถูกพิมพ์ค้างไว้ในไฟล์ เส้นทาง 3 · ทำให้มันอยู่ได้เป็นเดือน ไฟ · การกู้คืนตัวเองเมื่อค้าง · อัปเดตจากระยะไกล ดูแลอุปกรณ์เป็นร้อยตัวพร้อมกัน เส้นทาง 4 · จากหนึ่งตัวเป็นฝูง topic ที่ query ได้ · dashboard รวม · ใครเงียบเมื่อไร ลองออกแบบโครงสร้าง topic สำหรับ 50 อุปกรณ์

เส้นทาง 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

ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ 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

ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด — แสดงหน้าจอที่ 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 ไม่มี error
  • ลูกศรเปลี่ยนเดือนที่หัวปฏิทินไม่ส่ง event เฟิร์มแวร์กรองทิ้งด้วย lv_calendar_get_pressed_date() ที่ตอบไม่ผ่านเมื่อไม่ได้แตะวัน — รู้ได้เฉพาะตอนคนแตะวัน ไม่ใช่ตอนคนพลิกดูเดือน · ค่าที่ส่งมาคือเลขแปดหลัก YYYYMMDD ก้อนเดียว ต้องแกะเอง

machine.RTC มีอยู่จริงทั้ง Eva Kit และ Dev Kit (ตาราง modmachine.c ของพอร์ตร่วมใส่ไว้โดยไม่มีเงื่อนไข) ต่างจาก machine.PWM และ machine.ADC ที่ไม่มี — เรียกได้เลยโดยไม่ต้อง try/except

หน้าจอของทุกไฟล์ในคาบนี้ (1/2)

01 สามสถานะ และเส้นแบ่งที่ต้องตัดสินใจไว้ล่วงหน้า · 02 ต้องเห็นติดกันกี่รอบถึงจะเชื่อ · 03 ต่อใหม่แบบถอยห่างขึ้นเรื่อย ๆ ไม่ใช่รัวติดกัน
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

หน้าจอของทุกไฟล์ในคาบนี้ (2/2)

04 ข้อความสองชนิด สองจังหวะ คนละหน้าที่ · 05 เน็ตหลุดแล้วจอต้องยังทำงาน · 06 วงจรเต็มสี่ขั้นในไฟล์เดียว
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

คลังตัวอย่างประยุกต์ — เอาไปต่อยอดเองได้ (1/2)

01 เสาไฟสถานะแบบโรงงาน (andon light) · 02 ไฟหัวใจเต้น บอกว่าลูปยังไม่ตาย · 05 ปุ่มเดียว สองความหมาย
ไฟล์อยู่ที่ examples/usecase/ — ไม่ได้อยู่ในคาบไหนโดยเฉพาะ หยิบไปใช้กับงานจบได้เลย

คลังตัวอย่างประยุกต์ — เอาไปต่อยอดเองได้ (2/2)

07 กดค้างเพื่อยืนยันคำสั่งที่ย้อนกลับไม่ได้ · 14 การคาลิเบรตเข็มทิศ เป็นสิ่งที่วัดได้
ไฟล์อยู่ที่ examples/usecase/ — ไม่ได้อยู่ในคาบไหนโดยเฉพาะ หยิบไปใช้กับงานจบได้เลย

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

สถาปัตยกรรมและโพรโทคอล

บอร์ดและซอฟต์แวร์

  • KIT_PSE84_EVAL PSOC™ Edge E84 Evaluation Kit guide, Infineon 002-39007 Rev.*B
  • TESAIoT Dev Kit — SoM ของ KIT_PSE84_AI บนฐาน QWA309 (TESAIoT_KIT_PSE84_AI-Micropython-BentoClaw, bsps/TARGET_KIT_PSE84_AI/bsp_features.mk: เพิ่ม DPS368 · SHT40 · เรดาร์) — ภาพบอร์ดจริงยังไม่ได้ถ่าย
  • BMI270 datasheet — Bosch Sensortec — https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi270/
  • MicroPython documentation — https://docs.micropython.org/

ภาพประกอบ

  • IoT-Enabled Smart City Framework: National Institute of Standards and Technology / Wikimedia Commons — สาธารณสมบัติ
  • สัญญาณความสั่นลูกปืนปกติเทียบกับลูกปืนเสีย: Kolok P. et al., Sensors 25(21):6610 (2025) — CC BY 4.0 — https://pmc.ncbi.nlm.nih.gov/articles/PMC12609400/
  • Envelope spectrum ของลูกปืน: Mika D., Józwik J., Ruggiero A., Sensors 25(23):7371 (2025) — CC BY 4.0 — https://pmc.ncbi.nlm.nih.gov/articles/PMC12694681/
  • สถาปัตยกรรม MQTT pub/sub: Chine3me / Wikimedia Commons — CC0 1.0
  • Edge computing: Psenda38 / Wikimedia Commons — CC0 1.0
  • ไดอะแกรมอื่นทุกภาพในเด็คนี้วาดขึ้นใหม่สำหรับหลักสูตรนี้
  • เด็คนี้มีภาพสัญญาอนุญาต CC BY-SA 4.0 (ผังสถาปัตยกรรม IIoT) จึงเผยแพร่เอกสารชุดนี้ภายใต้ CC BY-SA 4.0

วิดีโอที่ตรวจแล้วว่าเปิดได้ (บันทึกใน _BUILD/MEDIA_PLAN.md)

อ้างอิง (ต่อ) — ข้อเท็จจริงของเฟิร์มแวร์ในเด็คนี้ ตรวจจากซอร์สโดยตรง

  • ไมโครโฟนใช้จาก Python ได้: โมดูล 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 ไม่ใช่ของห้อง
  • Eva Kit ไม่มีเซนเซอร์อุณหภูมิห้องหรือความชื้น · Dev Kit มี 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)
  • ห้าไฟล์ของคาบ 9–12 ที่ส่งอุณหภูมิ (s09/09 s10/08 s11/07 s12/06 s12/09) อ่านผ่าน read_temp(): sensors.sht40 เมื่อบอร์ดมี ไม่งั้นลูกบิดแทน (0–100 % = 15–45 °C) และบอกบน console — ยังไม่ได้รันบนบอร์ดจริงทั้งสองทาง (รอรอบทดสอบกับอาจารย์)

ทุกข้อจำกัดข้างบนมี path และเลขบรรทัดกำกับ ไม่ได้มาจากเอกสารฉบับใด ถ้าเจอที่ไม่ตรงกับบอร์ด บอกผู้สอนได้เลย

Tileview — นิ้วสั่งได้ โปรแกรมสั่งไม่ได้

ภาพจากตัวจำลอง Eva Kit (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 หนึ่ง บวกช่องละหนึ่ง

ข้อจำกัดที่ต้องออกแบบเผื่อ

  • โปรแกรมสั่งเลื่อนไปช่องที่ต้องการเองไม่ได้ — ไม่มี property สำหรับเลือกช่อง
  • ถามว่า "ตอนนี้อยู่ช่องไหน" ตรง ๆ ไม่ได้ ต้องอนุมานจาก 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

☰ สารบัญ