| คำถาม | คำตอบของคาบนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| Why | ของทุกชิ้นก็ทำงานได้อยู่แล้วตั้งแต่คาบ 5-7 ทำไมต้องเอามารวมจอเดียว | เพราะแผงควบคุมในโรงงานไม่ได้ออกแบบให้ "สวย" มันออกแบบให้ คนที่เหนื่อยและรีบ อ่านถูกในครั้งแรก · และของที่ทำงานได้ทีละชิ้น กับของที่ทำงานพร้อมกันทั้งจอภายใต้เพดาน 64 widgets เป็นคนละโจทย์กัน | ครึ่งแรก · HMI · ลำดับสายตา · สีมีความหมาย |
| What | มีอะไรให้ใช้บ้าง | ของใหม่มีสองชนิดคือ ui.Panel กับ ui.Compass · บวกโมดูล mic ทั้งแปดชื่อ สำหรับการ์ดใบที่ห้าที่ทีมเลือกได้ และ sensors.bmm350 ทั้งห้าชื่อ ที่ป้อนเข็มทิศ |
สไลด์ bmm350 ห้าชื่อ + สไลด์ mic แปดชื่อ |
| How | ประกอบยังไงให้ใช้งานได้จริง | นับ widget บนกระดาษให้ครบก่อนพิมพ์ (จองไป 23 จากงบ 32 ที่ตั้งไว้เอง — เพดานเฟิร์มแวร์คือ 64) → คำนวณพิกัดสี่การ์ดเอง → อ่านเซนเซอร์ทุกตัวในลูปเดียวที่ cadence 200 ms → รันยาวสิบนาทีเพื่อพิสูจน์ | หกไฟล์ตัวอย่าง + ใบฝึก + soak run |
ปลายทางที่จับต้องได้ — แดชบอร์ดสี่การ์ดในจอเดียว IMU · เข็มทิศ · CapSense · ลูกบิด ที่รันต่อเนื่องสิบนาทีโดยไม่ค้างและไม่ crash
คาบ 7 เราทำกราฟหนึ่งใบให้ลื่น · คาบนี้ต้องทำให้ของทั้งจอลื่นพร้อมกัน ภายใต้งบที่นับได้
ui.Panel สี่ใบเข้ากับ Chart, Compass, Bar, Arc, Seg7 ให้เป็นหน้าจอเดียวที่อ่านรู้เรื่องปลายทางของวันนี้: จอบอร์ดขึ้นแดชบอร์ดสี่การ์ดของทีมเรา รันยาวสิบนาทีโดยไม่มีอะไรสะดุด
คาบนี้คือ capstone ครึ่งทาง — ไม่มีของใหม่มาก แต่ต้องเอาของเก่าทั้งหมดมาอยู่ร่วมจอเดียวกันให้ได้

งบ widget มีจำกัด การจัดวางจึงเป็นการตัดสินใจ ไม่ใช่การตกแต่ง
| จากคาบ | สิ่งที่ทำได้แล้ว | วันนี้กลายเป็น |
|---|---|---|
| คาบ 5 | pot → ui.Arc + ui.Seg7, CapSense slider → ui.Bar |
การ์ดสองใบล่าง |
| คาบ 6 | sensors.bmi270.motion() อ่าน 6 แกนใน lock เดียว |
แหล่งข้อมูลของกราฟ |
| คาบ 7 | ui.Chart หลาย series + cadence 200 ms |
การ์ด IMU ซ้ายบน |
ของใหม่วันนี้มีสองอย่าง: ui.Panel เป็นกรอบการ์ด · ui.Compass กินค่าจาก bmm350.heading()
ที่เหลือคืองานออกแบบ — จัดของที่มีอยู่แล้วให้อยู่ร่วมจอเดียวกันโดยไม่ทับกัน ไม่เกินงบ และยังอ่านออก
เนื้อหาใหม่น้อย แต่ความยากขึ้นชัด เพราะครั้งนี้ทุกชิ้นต้องทำงานพร้อมกัน

ดูเพิ่ม (7 นาที): Projected Capacitive Touch Technology - How It Works — นิ้ว "ขโมย" เส้นสนามไฟฟ้า คือค่าที่ capsense.read() คืนมา
แผงควบคุมในโรงงานไม่ได้ออกแบบให้ "สวย" มันออกแบบให้ คนที่เหนื่อยและรีบ อ่านถูกในครั้งแรก
หลักข้อแรกคือการจัดกลุ่ม: ค่าที่มาจากแหล่งเดียวกันหรือใช้ตัดสินใจเรื่องเดียวกัน ต้องอยู่ในกรอบเดียวกัน
ลองคิดถึงหน้าจอที่มีตัวเลขสิบห้าตัวเรียงกันเป็นแถวยาวโดยไม่มีกรอบอะไรเลย ต่อให้ตัวเลขถูกทุกตัว คนดูก็ต้องอ่านป้ายกำกับทีละตัวเพื่อหาว่าอันไหนคืออันที่ตัวเองต้องการ นั่นคือเวลาที่เสียไปทุกครั้งที่มีคนมองจอ
การ์ดหนึ่งใบตอบคำถามหนึ่งคำถาม: "การเคลื่อนไหวเป็นยังไง" · "หันหน้าไปทางไหน" · "มีใครแตะอยู่ไหม" · "ลูกบิดตั้งไว้เท่าไร"
ถ้าตอบไม่ได้ว่าการ์ดใบนี้ตอบคำถามอะไร แสดงว่ายังไม่ควรมีการ์ดใบนี้
บนหน้าจอเดียวกัน ของทุกชิ้นไม่ได้สำคัญเท่ากัน เราต้องตัดสินใจแทนคนดูว่าอะไรควรถูกเห็นก่อน
สามชั้นที่ใช้จริงในงาน HMI
ใน ui เรามีเครื่องมือคุมสามชั้นนี้อยู่แค่สองอย่าง: value= ของ Label (ขนาดฟอนต์ 14/16/20/24/28) และ color= เท่านั้น จึงต้องใช้ให้ตรงเป้า อย่าใส่ 28 ให้ทุกตัวเพราะ "ใหญ่แล้วดูดี" — ถ้าทุกอย่างเด่น แปลว่าไม่มีอะไรเด่น
ในโค้ดวันนี้ ตัวเลของศาของเข็มทิศได้ 28 px ส่วนชื่อการ์ดได้ 20 px และหน่วยได้สีเทา นั่นคือการตัดสินใจ ไม่ใช่ความบังเอิญ

ขนาดฟอนต์คือการประกาศว่า "ของชิ้นนี้สำคัญกว่าชิ้นนั้น" — ประกาศให้ตรงกับความจริง
ในแผงควบคุมจริง สีสามสีนี้ถูกจองไว้แล้ว และคนทั้งอุตสาหกรรมอ่านมันตรงกัน
| สี | ความหมาย | คนดูควรทำอะไร |
|---|---|---|
| เขียว | ปกติ อยู่ในเกณฑ์ | ไม่ต้องทำอะไร |
| เหลือง/อำพัน | เฝ้าดู เริ่มออกนอกช่วงที่คุ้นเคย | กลับมาดูอีกที |
| แดง | ต้องลงมือ | หยุด/แก้ เดี๋ยวนี้ |
กฎที่ตามมาคือ ห้ามใช้แดงกับของที่ปกติ ถ้าเราทำกรอบการ์ดเป็นสีแดงเพราะชอบสีแดง วันที่เกิดเรื่องจริงจะไม่มีใครสังเกตเห็น

สีที่เหลือ (ม่วง · เขียวน้ำทะเล · ม่วงกล้วยไม้ · ฟ้า — จานสีเส้นข้อมูลของ SPEC §S7.13) ใช้เพื่อ แยกแหล่งข้อมูล ไม่ใช่บอกสถานะ — พื้นการ์ดกับตัวหนังสือยืมจาก ui_theme.py ในชุดตัวอย่างที่มากับเฟิร์มแวร์ หน้าจอของเราจะดูเป็นระบบเดียวกับเมนูอื่น · บรรทัดจริงจาก solution_codes/s08_dashboard.py↗:
BG_CARD = 0x171B22 # พื้นการ์ด เทาเข้มอมน้ำเงิน
COL_WHITE = 0xE8EAED # ตัวเลขพระเอก
COL_GRAY = 0x9AA3AF # ข้อความประกอบ / สถานะเงียบ เทาอ่อน
COL_IMU = 0x8E7BFF # BMI270 - เส้นที่ 2 ของจานสีเส้นข้อมูล สีม่วง
COL_COMP = 0x2FB6A8 # BMM350 - เส้นที่ 3 ของจานสีเส้นข้อมูล สีเขียวน้ำทะเล
COL_TOUCH = 0xC77DFF # CapSense - เส้นที่ 4 ของจานสีเส้นข้อมูล สีม่วงกล้วยไม้
COL_POT = 0x4A9EFF # Potentiometer - เส้นที่ 1 ของจานสีเส้นข้อมูล สีฟ้า
COL_STAT = 0x30A46C # เขียว "ปกติ"
COL_WARN = 0xF5A623 # เหลือง "ค่าเชื่อไม่ได้"
COL_ALERT = 0xE5484D # แดง "ต้องลงมือ"
ให้คัดค่าสีที่ใช้จริงมาวางไว้ต้นไฟล์ของเราเอง (อย่างที่เห็นข้างบน) แทนการ import — ชื่อในไฟล์ต้นทางไม่ตรงกับของเราทุกตัว เช่นสีเตือนของเราชื่อ
COL_ALERTส่วนต้นทางเรียกว่าCOL_AXและอย่าสุ่มเลขสีเองเด็ดขาด
สัญชาตญาณบอกว่ายิ่งอัปเดตถี่ยิ่งดี ในงาน HMI มันไม่จริง
ตัวเลขที่เปลี่ยนทุก 20 ms คือตัวเลขที่ มนุษย์อ่านไม่ทัน สายตาเห็นเป็นเลขเบลอ ๆ ที่กระพริบ ส่วนกราฟที่เลื่อนเร็วเกินก็ดูไม่ออกว่าแนวโน้มขึ้นหรือลง
ที่ 200 ms คนอ่านตัวเลขทันพอดี (ห้าครั้งต่อวินาที) กราฟเลื่อนแบบเห็นทิศทาง และ CM55 มีเวลาวาดครบทุกเฟรม
อีกด้านหนึ่ง — ด้านของเครื่อง ลูปที่เร็วเกินจะส่งงานให้ CM55 ถี่กว่าที่มันวาดทัน คิวเต็ม แล้วเฟรมจะหายไปเงียบ ๆ ไม่มี error ให้เห็น หน้าจอแค่ "รู้สึกกระตุก" ซึ่งเป็นอาการที่ดีบักยากที่สุด
จำง่าย ๆ: แดชบอร์ดหนัก 200 ms · หน้าเบา ๆ ที่มีไม่กี่ widget อย่างต่ำ 50 ms
การจำกัดความถี่ไม่ใช่การยอมแพ้เรื่องประสิทธิภาพ มันคือการออกแบบให้ตรงกับความเร็วของสายตาคน


ui ไม่มีระบบ layout อัตโนมัติแบบเว็บ (ไม่มี grid ไม่มี flex) ทุก widget ต้องบอกพิกัดเอง เราจึงต้องบวกลบเลขบนกระดาษก่อน
เลขที่ต้องตรวจให้ตรงเสมอ: x + w ต้องไม่เกิน 792 และ y + h ต้องไม่เกิน 398 (การ์ด 2: 402+382 = 784 เหลือขอบขวา 8 px พอดี)
วางของทับกันแล้วจอไม่ error มันแค่วาดทับ — เลขที่ผิดจะเงียบจนกว่าเราจะมองเห็นด้วยตา
ui.Panel — การ์ดหนึ่งใบ กับพารามิเตอร์ที่ชื่อไม่ตรงความหมายui.Panel ใช้ kwargs ชุดเดียวกับ widget อื่น แต่ ความหมายไม่เหมือนใคร จุดนี้พลาดกันทุกปี
| kwarg | สำหรับ Panel แปลว่า | ค่าที่เราใช้ |
|---|---|---|
color |
สีพื้นของการ์ด | BG_CARD = 0x171B22 |
min |
สีขอบ (ไม่ใช่ค่าต่ำสุด) | สีประจำเซนเซอร์ของการ์ดนั้น |
max |
รัศมีมุมโค้ง เป็นพิกเซล | 12 |
value |
ความหนาเส้นขอบ เป็นพิกเซล | 2 |
imu_panel = ui.Panel(x=24, y=100, w=368, h=136,
color=BG_CARD, min=COL_IMU, max=12, value=2)
เทียบกับ ui.Slider ที่ min/max/value เป็นตัวเลขค่าจริง ๆ และ ui.Label ที่ value คือขนาดฟอนต์ — ชื่อ kwarg เดียวกันเปลี่ยนความหมายไปตามชนิด widget
ลำดับการสร้างสำคัญ: สร้าง Panel ก่อน แล้วค่อยสร้าง Label/Chart ทับลงไป ของที่สร้างทีหลังอยู่ชั้นบน ถ้าสลับลำดับ การ์ดจะบังตัวหนังสือจนหาย
เวลาสงสัยว่า kwarg ตัวไหนแปลว่าอะไร ให้เปิดตารางนี้ อย่าเดาจากชื่อ
เฟิร์มแวร์รับได้ 64 widgets ต่อหนึ่งหน้าจอ (UI_MAX_WIDGETS ใน ipc_ui_protocol.h เท่ากันทั้งสองบอร์ด) ตัวที่ 65 จะไม่ขึ้น และมันไม่แจ้งเตือนอะไรเลย · ส่วน 32 คืองบที่คาบนี้ตั้งให้ตัวเอง เพื่อเหลือที่ไว้ให้คาบ 9-12 ต่อยอดบนหน้าจอใบเดิม
วิธีทำงานที่ถูกคือเขียนตารางนี้ก่อน แล้วรวมเลขให้เห็นก่อนแตะคีย์บอร์ด
| การ์ด | widget ที่ใช้ | จำนวน |
|---|---|---|
| หัวเรื่อง + แถบสถานะ | Label ชื่อทีม, Label รอบ, Label loop ms | 3 |
| 1 · IMU | Panel, Label หัวข้อ, Chart, Label ค่า 3 แกน | 4 |
| 2 · Compass | Panel, Label หัวข้อ, Compass, Label องศา, Label ทิศ | 5 |
| 3 · CapSense | Panel, Label หัวข้อ, Label ปุ่ม B0, Label ปุ่ม B1, Bar, Label % | 6 |
| 4 · Pot | Panel, Label หัวข้อ, Arc, Seg7, Label โวลต์ | 5 |
| รวม | 23 / 32 |
สังเกตว่าอะไรไม่นับ: series ของ Chart ไม่ใช่ widget (add_series() สามครั้งยังเป็น Chart ตัวเดียว) ส่วน Panel นับ ทุกใบ การ์ดสี่ใบจึงกินไปแล้ว 4 ตัวก่อนจะแสดงค่าอะไรเลย
เหลืองบ 9 ตัวไว้ให้ทีมต่อยอด — ใครจะเพิ่มการ์ดที่ห้า ต้องกลับมาแก้ตารางนี้ก่อน
ตารางนี้อยู่ในหัวไฟล์โค้ดด้วย เพราะคนที่กลับมาแก้ในอีกสองสัปดาห์คือตัวเราเอง
ตัวเลข 64 ไม่ได้มาจากความสวยงามของเลขยกกำลังสอง มันมาจากข้อจำกัดจริงของการคุยข้ามคอร์ — และเท่ากันทั้ง Eva Kit และ Dev Kit เพราะสองบอร์ดใช้ ipc_ui ชุดเดียวกัน
โค้ด Python ของเราอยู่บน CM33 ส่วน widget จริง ๆ ถูกสร้างและวาดโดย LVGL บน CM55 สองฝั่งนี้คุยกันผ่าน IPC ซึ่งมีพื้นที่หน่วยความจำร่วมขนาดคงที่ ทุก widget ที่สร้างจะกิน "ช่อง" ในตารางอ้างอิงฝั่ง CM55 หนึ่งช่อง และตารางนั้นถูกจองขนาดไว้ตายตัวตั้งแต่ตอนคอมไพล์ — จองแบบไม่ต้องขอหน่วยความจำเพิ่มตอนรัน เพราะการขอหน่วยความจำระหว่างวาดจอคือทางลัดสู่การค้าง
นี่คือแบบแผนที่เจอได้ทั่วไปในงาน embedded: จองล่วงหน้าในขนาดที่รู้แน่ ดีกว่ายืดหยุ่นแล้วเสี่ยงพังตอนทำงาน ระบบที่ต้องทำงานยาว ๆ โดยไม่มีคนดูแล เลือกทางแรกเสมอ
เชื่อมกับวันนี้: ตอนทีมนั่งเถียงกันว่าจะตัด Label ตัวไหนออกเพื่อให้พองบ 32 ที่ตั้งไว้ — นั่นคือการทำงานภายใต้ข้อจำกัดของหน่วยความจำจริง แบบเดียวกับที่วิศวกรออกแบบ HMI ในเครื่องจักรทำอยู่ทุกวัน และเป็นเหตุผลที่ MVP ของวันนี้วัดกันที่ "รันสิบนาทีไม่ค้าง" ไม่ใช่ "มีของบนจอเยอะที่สุด"
สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
วาดทุก widget ด้วย LVGL, จัดคิว IPC ข้ามคอร์, อ่านค่าจากเซนเซอร์ทั้งสี่ตัว, แปลงสนามแม่เหล็กเป็นองศา, จัดการ touch
สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่า ค่าไหนควรอยู่ด้วยกัน · ค่าไหนต้องเด่น · สีอะไรแปลว่าอะไร · อัปเดตถี่แค่ไหน · และงบที่มีจะแบ่งให้ใคร
ห้าข้อนี้ไม่มีข้อไหนเป็นเรื่องไวยากรณ์ภาษา Python เลย มันคืองานออกแบบล้วน ๆ ซึ่งเป็นงานที่ยังไงเครื่องก็ทำแทนเราไม่ได้
โค้ดวันนี้ยาวขึ้นก็จริง แต่ส่วนที่ยากคือตอนตัดสินใจก่อนพิมพ์ ไม่ใช่ตอนพิมพ์
import ui
import sensors
import time
ui.screen() # เปิดหน้าจอ Playground แบบว่าง (ล้าง widget เดิมทุกตัวให้เอง)
time.sleep_ms(200) # ให้ CM55 ตามทันก่อนเราเริ่มสร้างของใหม่
...
BG_CARD = 0x171B22 # พื้นการ์ด เทาเข้มอมน้ำเงิน
COL_WHITE = 0xE8EAED # ตัวเลขพระเอก
COL_GRAY = 0x9AA3AF # ข้อความประกอบ / สถานะเงียบ เทาอ่อน
...
COL_STAT = 0x30A46C # เขียว "ปกติ"
...
# ไม่มี sensors.init() ทั้งสองบอร์ด (Eva: ได้ OSError / Dev Kit: ไม่จำเป็น)
try:
sensors.bmi270.motion() # อ่านทิ้งหนึ่งครั้ง ให้การรอไปเกิดตรงนี้
except OSError:
print("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ในลูป")
เรียกอ่านค่าครั้งแรกแล้วเงียบไปสิบกว่าวินาที นั่นคือการรอ ไม่ใช่การค้าง (Eva วัดได้ถึง 16 วินาที · Dev Kit ยังไม่ได้วัด) — อย่าเพิ่งถอดสาย USB และบน Dev Kit ห้ามโยกสวิตช์บนฐาน นั่นคือสวิตช์ไฟ
ห้ามเรียก sensors.init() บน Eva Kit — เฟิร์มแวร์ปฏิเสธด้วย OSError เพราะคอร์จอ (CM55) เป็นเจ้าของบัส I2C ของเซนเซอร์ทั้งชุด ขับบัสซ้อนแล้วบอร์ดค้างจนต้องถอดไฟ · บน Dev Kit บรรทัดนั้นผ่านแต่ไม่จำเป็น เฟิร์มแวร์ปลุกทุกตัวไว้ตั้งแต่บูต — ไฟล์ของคอร์สจึงไม่มี init() และรันได้ทั้งสองบอร์ด
ค่ามาจากไหน — Eva: CM55 อ่านเซนเซอร์ให้ทุก 200 ms แล้วเก็บเป็นภาพสแกน · Dev Kit: CM33 อ่าน IMU และลูกบิดตรงจากบัสของตัวเอง ส่วนแถบสัมผัสถามคอร์จอ · ui.screen() ล้าง widget เดิมทั้งหมด ไม่เรียกแล้วของจากสคริปต์ก่อนหน้าจะกินงบตั้งแต่ยังไม่เริ่ม
imu_panel = ui.Panel(x=24, y=100, w=368, h=136,
color=BG_CARD, min=COL_IMU, max=12, value=2)
imu_title = ui.Label("ความเร่ง BMI270 (m/s2)", x=40, y=108, color=COL_IMU, value=16)
# Chart เก็บ 50 จุด รับเฉพาะจำนวนเต็ม เราจึงคูณ 10 ก่อนป้อน (-150..150 = -15.0..+15.0)
imu_chart = ui.Chart(x=40, y=136, w=336, h=56, min=-150, max=150, color=COL_IMU)
sy = imu_chart.add_series(COL_STAT) # แกน Y เขียว
sz = imu_chart.add_series(0x448AFF) # แกน Z ฟ้า
imu_val = ui.Label("X+0.0 Y+0.0 Z+9.8", x=40, y=200, color=COL_WHITE, value=20)
ui.Chart รับเฉพาะ จำนวนเต็ม เราจึงคูณค่าจริงด้วย 10 ก่อนป้อน แล้วตั้งแกน −150 ถึง 150 (= −15.0 ถึง +15.0 m/s²) ป้อน int(ax) ตรง ๆ ความละเอียดจะเหลือขั้นละ 1 m/s² กราฟเป็นขั้นบันได · series แรกได้สีจาก color= ของ Chart (COL_IMU สีม่วงประจำการ์ด ไม่ใช่สีเตือน) series ที่สองและสามได้จาก add_series() ซึ่งคืน index มาให้เก็บไว้ใช้ตอน set_next() · imu_val วางที่ y=200 คือใต้กราฟแต่ยังอยู่ในกรอบการ์ด (100 + 136 = 236)


กราฟบอกแนวโน้ม ตัวเลขบอกค่าปัจจุบัน — ใส่คู่กันเสมอ อย่างละครึ่งไม่พอสำหรับคนตัดสินใจ
comp_panel = ui.Panel(x=408, y=100, w=360, h=136,
color=BG_CARD, min=COL_COMP, max=12, value=2)
comp_title = ui.Label("เข็มทิศ BMM350", x=424, y=108, color=COL_COMP, value=16)
compass = ui.Compass(x=424, y=132, w=96, h=96, color=COL_COMP)
comp_deg = ui.Label("000 deg", x=544, y=140, color=COL_WHITE, value=28)
comp_dir = ui.Label("N", x=544, y=188, color=COL_COMP, value=24)
...
DIRS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
...
comp_dir.text(DIRS[int((heading + 22.5) / 45.0) % 8])
ui.Compass ใช้ w เป็น เส้นผ่านศูนย์กลาง — ไฟล์ส่ง h เท่ากันไว้ด้วย เพื่อให้ด่านตรวจพิกัดอ่านขนาดของมันได้ · รับค่าทิศผ่าน .value(องศา) โดย 0 คือทิศเหนือ · สีของเข็มทิศคือ COL_COMP สีประจำการ์ด ไม่ใช่สีเตือน
หน้าปัดกินซ้าย ตัวเลขกินขวา ตัวเลของศาได้ฟอนต์ 28 ใหญ่ที่สุดในหน้าจอ เพราะเป็นค่าที่ต้องอ่านจากไกลที่สุด ส่วนตัวอักษรทิศได้ 24 · แปลงองศาเป็นชื่อทิศด้วยเลขคณิตบรรทัดเดียว ไม่ต้องมี if ยาว ๆ — บวก 22.5 ก่อนหารด้วย 45 คือ "เลื่อนขอบช่อง" ให้ทิศเหนือกินช่วง 337.5–22.5 องศา แทนที่จะเป็น 0–45

ตัวเลขให้ความแม่นยำ ตัวอักษรทิศให้ความหมาย คนที่ยืนดูใช้อย่างหลังก่อนเสมอ
sensors.bmm350 มีห้าชื่อ — และหนึ่งในนั้นเรายังตอบเรื่องหน่วยไม่ได้การ์ดเข็มทิศใช้ heading() ตัวเดียว แต่ชิปเปิดให้เราอีกสี่ชื่อ ทุกตัว ไม่รับอาร์กิวเมนต์เลย และโยน OSError เมื่ออ่านไม่สำเร็จ
| เรียกอะไร | คืนอะไรกลับมา | ใช้ตอนไหน |
|---|---|---|
heading() |
float 0 ถึง 360 องศา 0 คือทิศเหนือ |
ค่าเดียวที่การ์ดเข็มทิศต้องใช้ |
magnetic() |
tuple สามค่า (mx, my, mz) เป็น float |
อยากเห็นสามแกนดิบ เช่นตรวจว่ามีแม่เหล็กเข้ามาใกล้ |
cal_reset() |
None และ พิมพ์หนึ่งบรรทัดลง REPL ว่าให้หมุนบอร์ดครบรอบ |
เริ่มคาลิเบรต hard-iron ใหม่ |
cal_status() |
dict สามคีย์ {'valid': bool, 'offset_x': float, 'offset_y': float} |
ดูว่าค่าชดเชยลู่เข้าหรือยัง |
chip_id() |
int |
ยืนยันว่ากำลังคุยกับชิปถูกตัว ตอนสงสัยว่าสายหลุด |
สามข้อที่ทำให้เข็มทิศบนการ์ดนิ่งหรือไม่นิ่ง
heading() เท่านั้นที่ขยับตัวคาลิเบรต — magnetic() ไม่ได้ป้อนอะไรให้มันเลย โปรแกรมที่เรียกแต่ magnetic() จะรอค่าชดเชยจนวันตายก็ไม่ลู่เข้าheading() คืนค่าเฉลี่ยของสิบครั้งหลังสุด เฉลี่ยแบบวงกลมเพื่อไม่ให้พังตอนข้ามรอยต่อ 0 กับ 360 องศา สิบครั้งแรกหลังบูตจึงเป็นค่าที่ยังเฉลี่ยไม่เต็มถัง — อย่าเพิ่งตัดสินจากค่าแรกcal_status()['valid'] เป็น True ก็ต่อเมื่อครบสองเงื่อนไข คือเก็บครบ 50 ตัวอย่าง และ ค่าที่กวาดได้กระจายพอทั้งสองแกน หมุนบอร์ดไม่ครบรอบ เงื่อนไขที่สองไม่ผ่าน · cal_reset() ไม่ได้เขียนอะไรลงชิป มันล้างตัวแปรฝั่งบอร์ดเท่านั้น ค่าคาลิเบรตจึงหายทุกครั้งที่รีเซ็ตชิปตัวนี้ตั้ง ODR ไว้ที่ 25 Hz อ่านถี่กว่านั้นจะได้ค่าเดิมซ้ำ ไม่ใช่ค่าใหม่ — ลูป 200 ms ของเราอยู่ห่างจากเพดานนี้มาก จึงไม่ต้องกังวล
sensors.bmm350 — ข้อบกพร่องที่ยังเปิดอยู่สองข้อ พูดตรง ๆ ดีกว่าให้ทีมไปเจอเองหนึ่ง หน่วยของ magnetic() ตอนนี้ตอบไม่ได้ว่าคืออะไร คอมเมนต์ในไดรเวอร์และป้ายในไฟล์ตัวอย่างเขียนว่า uT (ไมโครเทสลา) แต่ภาพที่จับจากบอร์ดวัดขนาดรวมสามแกนได้ราว 1532 ขณะที่สนามแม่เหล็กโลกทั้งใบอยู่ที่ 25 ถึง 65 uT — ตัวเลขกับหน่วยเป็นจริงพร้อมกันไม่ได้ อย่างน้อยหนึ่งอย่างผิด และเรายังไม่ได้ตัดสินว่าอันไหน
สิ่งที่ ไม่ ผิดคือทิศ: heading() วัดบนบอร์ดจริงได้ 215.8 เมื่อ 14 ส.ค. 2026 ซึ่งตรงกับความจริง เพราะการหาทิศใช้ atan2 ซึ่งสนใจแค่ อัตราส่วน ระหว่างสองแกน ตัวคูณที่ผิดเท่ากันทั้งคู่จึงหักล้างกันหมด นี่คือเหตุผลที่ข้อบกพร่องนี้อยู่มาได้นานโดยไม่มีใครสังเกต
กติกาของคาบนี้: ห้ามเขียนตัวเลขจาก magnetic() ลงรายงานพร้อมหน่วย uT ให้เขียนว่า "หน่วยของบอร์ด" และเทียบเป็น ค่าต่างจากเส้นฐานที่ทีมวัดเอง เสมอ — ซึ่งบังเอิญเป็นสิ่งที่ examples/s08/04_magnet_presence.py↗ กับ 05_door_open_switch.py ทำอยู่แล้วทั้งคู่ ทั้งสองไฟล์จึงยังใช้ได้ตามปกติ เกณฑ์ของมันเป็น "เบี่ยงไปเท่าไรจากตอนเริ่ม" ไม่ใช่ "กี่ไมโครเทสลา"
สอง cal_reset() กับ cal_status() ยังไม่มีใครรันบนบอร์ดจริงแม้แต่ครั้งเดียว ทั้ง Eva Kit และ Dev Kit ไฟล์ที่ใช้มันคือ examples/usecase/14_hard_iron_calibration.py↗ และมันเขียนคำเตือนข้อนี้ไว้ในหัวไฟล์ของตัวเอง เปิดได้ ลองได้ แต่ถ้าได้ OSError นั่นคือข้อมูลใหม่ที่ยังไม่มีใครมี ให้บอกผู้สอน
ตัวเลขที่ถูกกับหน่วยที่ถูกเป็นคนละเรื่องกัน — ระบบวัดที่บอกไม่ได้ว่าหน่วยของตัวเองคืออะไร ยังไม่ใช่ระบบวัดที่เสร็จ
cap_led0 = ui.Led(x=40, y=288, w=48, h=48, color=COL_TOUCH, value=0)
ui.Label("ปุ่ม 0", x=96, y=300, color=COL_GRAY, value=16)
cap_led1 = ui.Led(x=160, y=288, w=48, h=48, color=COL_TOUCH, value=0)
ui.Label("ปุ่ม 1", x=216, y=300, color=COL_GRAY, value=16)
cap_pct = ui.Label("0 %", x=272, y=256, color=COL_WHITE, value=20)
cap_bar = ui.Bar(x=40, y=348, w=288, h=16, min=0, max=100, value=0, color=COL_TOUCH)
...
pot_arc = ui.Arc(x=376, y=288, w=88, h=88, min=0, max=100, value=0)
pot_seg7 = ui.Seg7(x=480, y=288, w=112, h=44, color=COL_POT, min=0, max=100)
pot_volt = ui.Label("0.000 V", x=480, y=340, color=COL_WHITE, value=20)
ทำไมการ์ดสัมผัสใช้ Bar ไม่ใช่ Slider ทั้งที่หน้าตาคล้ายกัน — เพราะ Slider เป็นของที่ คนลาก ส่วน Bar เป็นของที่ เครื่องแสดง ค่าที่นิ้วเลื่อนอยู่บนแถบ CapSense จริง ๆ ถ้าใช้ Slider คนดูจะเข้าใจผิดว่าลากบนจอได้ นี่คือหลัก HMI ที่เรียกว่า ความสอดคล้องระหว่างหน้าตากับสิ่งที่ทำได้จริง · การ์ด pot ใช้สองตัวคู่กันด้วยเหตุผลเดียวกับกราฟ+ตัวเลข: Arc ตอบว่า "อยู่ตรงไหนของช่วง" ในพริบตา ส่วน Seg7 ให้ตัวเลขที่จดลงใบงานได้
ปุ่ม CapSense ใช้ ไฟสองดวง แทนป้ายที่เปลี่ยนสี — ถ่ายจอเป็นขาวดำแล้วยังแยกออกว่าดวงไหนติด · ไฟหรี่ ไม่ใช่หาย ตอนไม่ได้แตะ:
cap_led0.value(1 if cap['btn0'] else 0)
cap_led1.value(1 if cap['btn1'] else 0)
cap_bar.value(int(cap['slider']))

เลือก widget ตาม "ใครเป็นคนเปลี่ยนค่านี้" ไม่ใช่ตามว่าอันไหนดูดีกว่า
while True:
t0 = time.ticks_ms()
ok = True
if running:
try:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
except OSError:
ok = False # คงค่าเดิมไว้ ไม่เขียนศูนย์ทับ
imu_chart.set_next(0, int(ax * 10))
...
if time.ticks_diff(time.ticks_ms(), last_text) >= UI_TEXT_MS:
last_text = time.ticks_ms()
...
head.text("รอบที่ {} | loop {} ms".format(
rounds, time.ticks_diff(time.ticks_ms(), t0)))
rounds += 1
for ev in ui.poll():
...
time.sleep_ms(200)
กฎเหล็กข้อ 4 มีผลเต็ม ๆ ตรงนี้: พอเรียก ui.* ครั้งแรก ระบบอ่านเซนเซอร์อัตโนมัติของเฟิร์มแวร์จะหยุด เราจึงต้องอ่านเองทุกตัวในลูป แบบ synchronous เรียกแล้วรอค่าตรงนั้น ทั้งสี่ตัวเรียงกันในรอบเดียว
motion() ให้หกแกนจากการอ่านครั้งเดียว เร็วกว่าเรียก acceleration() แล้ว gyroscope() แยกสองครั้ง เช่นเดียวกับ capsense.read() ที่คืนสองปุ่มและ slider ในครั้งเดียว — หนึ่งรอบลูป แตะเซนเซอร์ให้น้อยครั้งที่สุด
ui.poll() ต้องเรียกทุกลูป — คาบนี้มีปุ่มเดินหน้า/หยุดภาพและ Spinbox ให้กดจริง แต่ต่อให้หน้าไหนไม่มีปุ่มก็ต้องเรียก เพราะมันคือจังหวะที่ CM55 ได้ระบายคิว ไม่เรียกแล้วจอจะซ่อน widget ราวสองวินาทีแล้วกลับมาใหม่วน ๆ · except ไม่เขียนศูนย์ทับค่าเดิม แค่ยกธง ok แล้วไฟค่าค้างเป็นคนบอก · t0 ต้นลูปแล้ววัดเวลาที่ใช้ไป คือเครื่องมือหลักตอน soak test
ทุกบรรทัดในลูปนี้จะถูกรันประมาณสามพันรอบระหว่างการทดสอบสิบนาที บรรทัดที่แพงจะโผล่ให้เห็นเอง
ดูเพิ่ม (1 นาที): Working principle of an accelerometer — Bosch Sensortec — 1:01 — ผู้ผลิต BMI270 บนบอร์ดเราแสดงให้เห็นว่ามวลพิสูจน์ในชิปขยับจริง ซึ่งคือที่มาของเส้นกราฟสามเส้นนี้
เซนเซอร์สี่ตัวไม่ได้วิ่งขนานกัน มันต่อคิวกันอยู่ในลูปเดียวของเรา — นี่คือเหตุผลที่ต้องอ่านให้น้อยครั้งที่สุด
mic ทั้งแปดชื่อไมโครโฟนบนบอร์ดใช้จาก Python ได้แล้ว โมดูล mic ฝังมากับเฟิร์มแวร์ ทั้ง Eva Kit, TESAIoT Dev Kit และ AI Kit import mic ได้เลย ไม่ต้องคัดลอกไฟล์ไปไว้บนบอร์ด และมีอยู่แปดชื่อพอดี
| เรียกอะไร | คืนอะไร | หมายเหตุ |
|---|---|---|
mic.start(sens=3, rate=16000, samples=256) |
True |
ต้องเรียกก่อนทุกอย่าง ตัวอื่นจะโยน OSError ถ้ายังไม่เปิด |
mic.stop() |
None |
ปิดแล้วคิวหยุดโต |
mic.read(n=None) |
list ของจำนวนเต็ม หักค่ากลางออกแล้ว ยาวเท่า samples (ตั้งต้น 256) |
ตัวเดียวที่ให้ รูปคลื่นดิบ · ไม่ทิ้งของค้าง อ่านของเก่าก่อน |
mic.stats(fresh=True) |
tuple สามค่า (rms, peak, dc) |
สามค่ามาจาก หน้าต่างเดียวกัน เป็นวิธีเดียวที่จะได้ชุดที่สอดคล้องกัน |
mic.rms() |
จำนวนเต็ม 0 ถึง 32768 | ความดังเฉลี่ยแบบยกกำลังสองก่อน |
mic.peak() |
จำนวนเต็ม 0 ถึง 32768 | ยอดสูงสุดในหน้าต่าง เห็นเสียงพุ่งสั้น ๆ ที่ rms() เฉลี่ยจนหาย |
mic.level() |
จำนวนเต็ม 0 ถึง 100 | สเกลอ็อกเทฟ ไม่ใช่เศษส่วนของสเกลเต็ม อ่านหัวข้อถัดไปก่อนใช้ |
mic.lag() |
จำนวนเต็ม มิลลิวินาที ที่ค้างอยู่ในคิว | ตัวเลขนี้ คือ ความหน่วงที่ตาเราเห็น ไม่ใช่ตัวประมาณ |
mic (ต่อ) — คิวเสียงยาว 625 ms และมันเสิร์ฟของเก่าก่อนไมค์ผลิตเสียงตลอดเวลา ไม่ว่าโปรแกรมเราจะอ่านหรือไม่ ตัวรับปลายทางคือวงแหวนที่จุได้ 624.9 ms พอดี (20,000 ไบต์ ที่ 16 kHz แบบสองไบต์ต่อตัวอย่าง) และมันจ่าย ตัวที่เก่าที่สุดก่อน
ผลคือ ลูปที่อ่านช้ากว่าที่ไมค์ผลิต จะตามหลังถาวร และเพดานของการตามหลังคือ 625 ms — สิ่งที่การ์ดแสดงจะเป็นห้องเมื่อครึ่งวินาทีที่แล้ว ไม่ใช่ห้องตอนนี้ ทางแก้มีอยู่แล้วในตัว: fresh=True ซึ่งเป็น ค่าตั้งต้น ของ stats() แปลว่า "ทิ้งของค้างทั้งหมด แล้วเอาเฉพาะหน้าต่างล่าสุด"
| อ่านแบบไหน | lag() ที่วัดได้จริงบนบอร์ด 14 ส.ค. 2026 |
|---|---|
stats(fresh=True) (ค่าตั้งต้น) |
0 ถึง 48 ms |
stats(fresh=False) — เก็บของค้างไว้ |
496 ถึง 624 ms |
read() ใช้ทางที่ ไม่ทิ้ง เสมอ อยากได้รูปคลื่นสด ๆ ต้องอ่านให้ทันเอง หรือเรียก stats() นำหน้าเพื่อเคลียร์คิวก่อน
level() เป็นสเกลอ็อกเทฟ ไม่ใช่เปอร์เซ็นต์ของสเกลเต็มไทย: ทุกครั้งที่ rms เพิ่มเป็นสองเท่า ตัวเลข level ขึ้นราว 9 หน่วย ไม่ใช่ขึ้นเป็นเท่าตัว สิบเอ็ดอ็อกเทฟถูกยืดลงบนช่วง 0 ถึง 100 พอดี
ตัวเลขจากบอร์ดนี้ ห้องเงียบ rms 30 ได้ level 7 · พูดปกติ rms ราว 1000 ได้ 54 · ตบมือหรือเปิดโทนใส่ไมค์ rms 19335 ได้ 92 — ถ้าใช้เศษส่วนของสเกลเต็มแบบตรง ๆ ทั้งห้องเงียบและเสียงพูดจะกลายเป็น 0 เท่ากันหมด นั่นคือเหตุผลที่มันเป็นอ็อกเทฟ
mic (ต่อ) — ราคาที่ต้องจ่าย และกับดักที่เจอบ่อยที่สุดmic.level() หนึ่งครั้งกินราว 32 ms วัดบนบอร์ด ในนั้น 16 ms คือตัวเสียงเอง (หน้าต่าง 256 ตัวอย่างที่ 16 kHz ก็คือ 16 ms ของเวลาจริง) ส่วนนั้น ลดไม่ได้ ที่เหลือคือค่าใช้จ่ายของการอ่าน — การ์ดเสียงที่ลูป 200 ms จึงจ่ายค่านี้ไปราวหนึ่งในหก ของงบเวลาทั้งรอบ
กับดัก: rms() peak() level() แต่ละตัว อ่านไมค์ใหม่หนึ่งครั้ง เรียก peak() แล้ว rms() ติดกันคือการอ่านสองหน้าต่างคนละช่วงเวลา และจ่ายค่า 32 ms ไปสองรอบ ต้องการทั้งคู่ให้เรียก stats() ครั้งเดียว แล้วแกะออกมาสามค่า
อีกสองข้อ: sens= รับ 1 ถึง 5 (ยิ่งมากยิ่งไวต่อเสียงเบา) ใส่ค่านอกช่วงมัน ตกกลับไปเป็น 3 เงียบ ๆ ไม่มี error ให้เห็น · read(n) ตัดให้สั้นได้อย่างเดียว ขอ 1000 จากบัฟเฟอร์ 256 จะได้ 256 ไม่ใช่ 1000 และมันไม่รอเก็บเพิ่มให้
ลองสามไฟล์ตามลำดับ: examples/s08/02_mic_sound_level_meter.py↗ (level()) · 03_mic_clap_trigger.py (peak() กับเกณฑ์ที่วัดจากห้องเอง) · 07_mic_window_stats.py (read() stats() lag() และการทดลองสลับ fresh ให้เห็นคิวโตกับตา)
ตัวเลขความดังที่อ่านมาช้ากว่าความจริงครึ่งวินาที ยังเป็นตัวเลขที่ถูก — แต่มันตอบคนละคำถามกับที่คนดูจอกำลังถาม
practise_codes/s08_dashboard.py↗ ใน BENTO IDEhead ให้เป็นของทีมเราถ้าจอค้างระหว่างทาง กด RESTART บนหน้า Playground แล้วเริ่มจับเวลาใหม่ตั้งแต่ศูนย์ — ห้ามนับต่อ
ห้ามเติมครบเจ็ดจุดแล้วค่อยรันทีเดียว ถ้าพังจะไม่รู้เลยว่าพังที่จุดไหนในเจ็ดจุด
การรันสิบนาทีไม่ใช่การนั่งรอเฉย ๆ เรากำลังเก็บข้อมูลอยู่ จดเลขรอบทุกสองนาที
ที่ cadence 200 ms ลูปควรเดินราว 5 รอบต่อวินาที = ประมาณ 600 รอบต่อสองนาที ถ้าจดได้ 590–600 ทุกช่วง แปลว่าคงที่ ถ้าช่วงหลังเหลือ 300 แปลว่าเริ่มช้าลงแล้ว
| อาการที่เห็น | เลขรอบ | loop ms | แปลว่า |
|---|---|---|---|
| ทุกอย่างนิ่งสนิท ภาพค้างที่เฟรมสุดท้าย | หยุด | หยุด | ค้างจริง — ลูปตายหรือ CM55 หยุดตอบ |
| ตัวเลขยังเดินแต่ช้าลงเรื่อย ๆ | เดินต่อ | โตขึ้น 3 → 40 | ช้า ไม่ใช่ค้าง — มีอะไรสะสมในลูป |
| จอนิ่ง แต่คอนโซลขึ้น Traceback | หยุด | หยุด | สคริปต์ตายที่ Python — อ่าน error ได้ตรง ๆ |
| widget หายไปแวบหนึ่งแล้วกลับมา | เดินต่อ | ปกติ | ลืม ui.poll() ในลูป |
| ค่าเซนเซอร์ค้างค่าเดิม แต่รอบยังเดิน | เดินต่อ | ปกติ | เซนเซอร์ไม่ตอบ ไม่ใช่จอมีปัญหา |
วิธีแยกที่เร็วที่สุด: หมุนลูกบิด แล้วดูสามที่พร้อมกัน — เลข Seg7, เลขรอบ, และ loop ms ถ้าเลขรอบเดินแต่ Seg7 ไม่ขยับ ปัญหาอยู่ฝั่งเซนเซอร์ ถ้าเลขรอบหยุด ปัญหาอยู่ฝั่งลูปหรือจอ
อีกอย่างที่ต้องดูคือ ความร้อนและการเสียบสาย — สาย USB ที่หลวมทำให้บอร์ดรีเซ็ตกลางทาง แล้วเราจะไปโทษโค้ดผิด ๆ
"ค้าง" กับ "ช้า" แก้คนละวิธีกันสิ้นเชิง เสียเวลาห้าวินาทีแยกให้ออกก่อน ประหยัดไปได้ครึ่งชั่วโมง
dashboard 4 การ์ดรันต่อเนื่อง 10 นาทีไม่ค้างไม่ crash
แปลเป็นสิ่งที่ตรวจได้จริง:
ui.Led ดวงนั้นติด (ปล่อยแล้วหรี่ ไม่ใช่หาย) · เลื่อนนิ้วแล้ว Bar กับตัวเลข % ขยับห้าข้อแรกใช้เวลาสร้างหนึ่งชั่วโมง ข้อที่เจ็ดใช้เวลาสิบนาทีที่ห้ามลัด — และมันคือข้อที่แยกของเล่นออกจากของใช้งานจริง
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| widget ตัวท้าย ๆ ไม่ขึ้นเลย | เกินเพดาน 64 (มักเพราะไม่ได้ ui.screen() ก่อน) |
นับใหม่ + เรียก ui.screen() ต้นสคริปต์ |
| การ์ดบังตัวหนังสือจนหาย | สร้าง Panel หลัง Label | สร้าง Panel ก่อน Label เสมอ |
| ขอบการ์ดไม่มีสี มุมไม่โค้ง | สับสน kwarg ของ Panel | min=สีขอบ max=รัศมี value=ความหนา |
| กราฟเป็นขั้นบันไดหยาบ ๆ | ป้อน int(ax) ตรง ๆ |
คูณ 10 ก่อน แล้วตั้งแกน −150..150 |
| จอกระพริบ ซ่อน widget ทุกสองวินาที | ลืม ui.poll() ในลูป |
เติม ui.poll() ก่อน sleep_ms |
OSError ที่บรรทัดแรกสุดของสคริปต์ |
เรียก sensors.init() — Eva Kit ปฏิเสธ (Dev Kit ผ่านเงียบ ๆ แต่ไม่จำเป็น) |
ลบบรรทัดนั้นทิ้ง ไม่ต้องมี init ทั้งสองบอร์ด |
| บรรทัดอ่านค่าแรกเงียบไปสิบกว่าวินาที | เพิ่งรีเซ็ตบอร์ด คอร์จอยังไม่ตอบสายเซนเซอร์ | รอให้จบ (บน Eva วัดได้ถึง 16 วินาที) ครั้งต่อไปไม่เกิน 1 |
| เข็มทิศกระตุกกลับตอนผ่านทิศเหนือ | ค่าองศาหลุดเกิน 360 | heading = ... % 360.0 |
| ค่าทุกอย่างค้าง แต่โปรแกรมไม่ error | ยังใช้ sensors.auto() ร่วมกับ ui (เกิดได้เฉพาะ Dev Kit — บน Eva Kit auto() ขึ้น OSError ตั้งแต่บรรทัดนั้น) |
ลบทิ้ง แล้วอ่าน sync ในลูปแทน ทั้งสองบอร์ด |
OSError ตอนเรียก sensors.scan() |
บน Eva Kit สแกน 112 แอดเดรสบนบัสที่ CM55 ถืออยู่ (Dev Kit ผ่าน) | ห้ามใช้ในคอร์สนี้ทั้งสองบอร์ด เฟิร์มแวร์ Eva กันไว้ให้แล้ว |
OSError ที่ bmi270.temperature() หรือ chip_id() |
บน Eva Kit ภาพสแกนของ CM55 ไม่ได้ขนสองค่านี้มา (Dev Kit ใช้ได้) | ใช้ sensors.snapshot()['bmi270']['sequence'] ดูว่า IMU ยังมีชีวิตแทน — รันได้ทั้งสองบอร์ด |
ส่วนใหญ่ของข้อเหล่านี้เงียบสนิท ไม่มี error message — ตาของเราคือเครื่องมือดีบักหลักในงาน UI
เปิด practise_codes/s08_dashboard.py↗ มีช่องว่างให้เติม 7 จุด
ห้ามเรียก
sensors.init()ทั้งสองบอร์ด — Eva Kit ได้OSErrorเสมอ Dev Kit ผ่านแต่ไม่จำเป็น — จุดที่ 1 ที่ต้องเติมคือ การอ่านทิ้งหนึ่งครั้ง ไม่ใช่การเปิดเซนเซอร์
try:
# เติม: sensors.bmi270.motion()
pass
except OSError:
pass
# เติม: imu_panel = ui.Panel(x=24, y=100, w=368, h=136, color=BG_CARD, min=COL_IMU, max=12, value=2)
pass
# เติม: imu_chart.set_next(sy, int(ay * 10))
pass
# เติม: compass.value(int(heading))
pass
# เติม: cap_bar.value(int(cap['slider']))
pass
# เติม: pot_seg7.text("{:.1f}".format(pct))
pass
# เติม: for ev in ui.poll():
pass
ลำดับที่แนะนำ: เติมจุดที่ 1–2 ก่อนแล้วรัน (ควรเห็นการ์ดว่างหนึ่งใบ) · เติม 3–6 ทีละจุดแล้วรัน (ค่าจะมาทีละการ์ด) · เติมจุดที่ 7 เป็นอันสุดท้าย — จงใจลองผิดหนึ่งรอบ: ก่อนเติมจุดที่ 7 ให้รันดูสักครึ่งนาที แล้วจดว่าจอมีอาการอะไร นั่นคืออาการของการลืม ui.poll()
เติมทีละจุดแล้วรัน คือวิธีเดียวที่ทำให้รู้ว่าจุดไหนพัง เมื่อโค้ดยาวเกินสามสิบบรรทัด
ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 30 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · เข็มทิศที่อ่านออก · 10 นาที | examples/s08/06_compass_readout.py↗ |
ประกอบการ์ด Compass ใบที่ 2 ของ dashboard ได้ครบทั้งเข็ม ตัวเลข และกราฟ · จะเห็นว่าเข็มทิศที่คนอ่านออกต้องมีทั้งเข็มและตัวเลของศาอยู่ด้วยกัน |
| 2 · เกณฑ์สองระดับกันป้ายกระพริบ · 10 นาที | examples/s08/05_door_open_switch.py↗ |
เขียนป้ายสถานะที่ยืนอยู่ได้จริงตลอด soak run 10 นาที ไม่สั่นตอนค่าคาบเส้น · จะเข้าใจว่าเกณฑ์สองระดับคือสิ่งที่ทำให้ป้ายสถานะไม่กระพริบ |
| 3 · การ์ดระดับเสียง · 10 นาที | examples/s08/02_mic_sound_level_meter.py↗ |
ประกอบการ์ดระดับเสียงจากไมโครโฟนได้ครบทั้งแถบ ตัวเลข และกราฟ · จะเห็นว่า mic.level() ยุบคลื่นเสียงทั้งชุดเหลือตัวเลขเดียวที่การ์ดใบหนึ่งแสดงได้ |
ไฟล์ที่ 1 ยืนยันบน Eva Kit แล้ว (heading() วัดจริงได้ 215.8 เมื่อ 14 ส.ค. 2026 · บน Dev Kit ยังไม่ได้รัน) · ไฟล์ที่ 2 ใช้ sensors.bmm350.magnetic() ซึ่ง ยังไม่มีใครรันบนบอร์ดจริงทั้ง Eva Kit และ Dev Kit ถ้าเปิดแล้วได้ OSError ให้ข้ามไปทำการ์ด IMU ก่อน แล้วแจ้งผู้สอน · ไฟล์ที่ 3 ยืนยันบน Eva Kit แล้วเช่นกัน (14 ส.ค. 2026 ห้องเงียบได้ rms 30 คือระดับ 7 · พูดปกติราว 1000 คือระดับ 54 · เปิดโทนใส่ไมค์ได้ 19335 คือระดับ 92)

ภาพซ้ายคือไฟล์ที่ 3 กำลังรันอยู่บนบอร์ดจริงในหน้า BENTO Playground เส้นกราฟขยับตามเสียงในห้องระหว่างที่ถ่ายภาพ ระดับตอนนั้นคือ 9 และดังสุดที่เจอคือ 23
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| เกณฑ์ที่ตั้งไว้ใช้ได้ที่โต๊ะนี้ แต่ย้ายโต๊ะแล้วเตือนรัว | examples/s08/04_magnet_presence.py↗ — วัดเส้นฐานของห้องเองตอนเริ่ม แล้วคิดเกณฑ์จากความผันผวนของห้องนั้น |
| กดปุ่มครั้งเดียว แต่ตัวนับบนการ์ดขึ้นหลายครั้ง | examples/s03/05_debounce_count.py↗ — นับดิบกับนับกันเด้งวิ่งคู่กันให้เห็นความต่าง |
| อยากให้การ์ดใบหนึ่งแสดง "ระดับ" ที่ตัดสินแล้ว ไม่ใช่ตัวเลขดิบ | examples/usecase/01_andon_severity_lamp.py↗ — หนึ่งระดับคือไฟหนึ่งดวง ดับให้หมดก่อนจุดดวงใหม่เสมอ และเขียนจอเฉพาะตอนระดับเปลี่ยน |
| อยากให้การ์ดตอบตอนตบมือ แต่ตัวเลขความดังเฉลี่ยแทบไม่ขยับ | examples/s08/03_mic_clap_trigger.py↗ — peak() เห็นเสียงพุ่งสั้น ๆ ที่ rms() เฉลี่ยจนหายไป และเกณฑ์ต้องวัดจากห้องที่บอร์ดอยู่ตอนนั้น |
| การ์ดเสียงขยับช้ากว่าที่พูดจริงราวครึ่งวินาที | examples/s08/07_mic_window_stats.py↗ — lag() บอกเป็นตัวเลขว่าคิวค้างกี่ ms และปุ่มสลับ fresh ให้เห็นคิวโตจนเต็ม 625 กับตา · ไฟล์นี้ยังเป็นที่เดียวที่ใช้ read() กับ stats() สามค่าจากหน้าต่างเดียว |
| เข็มทิศบนการ์ดใบที่ 2 ชี้ผิดทิศ หรือสั่นทั้งที่วางบอร์ดนิ่ง | examples/usecase/14_hard_iron_calibration.py↗ — ล้างค่าชดเชยแล้วหมุนเลขแปด เฝ้าดู offset_x offset_y ลู่เข้าจนเป็นเส้นแบน คาลิเบรตเป็นตัวเลขที่ดูได้ ไม่ใช่พิธีกรรมที่ทำแล้วหวังผล · แต่ cal_reset() กับ cal_status() ยังไม่มีใครรันบนบอร์ดจริง ทั้ง Eva Kit และ Dev Kit ถ้าเปิดแล้วได้ OSError ให้แจ้งผู้สอนแล้วกลับไปทำการ์ดใบอื่นก่อน |
| อยากได้ปุ่มสั่งงานเพิ่ม แต่ไม่อยากจ่ายงบ widget ให้ปุ่มบนจอ | examples/usecase/05_short_long_press.py↗ — ปุ่มผู้ใช้ที่เฟิร์มแวร์เปิดให้คือ gpio.button(0) ตัวเดียว (.name() คืน "USER Button 1" ทั้งสองบอร์ด) และไม่กินงบเลย · บน Dev Kit ห้ามโยกสวิตช์บนฐาน พวกนั้นคือสวิตช์ไฟ ไม่ใช่ปุ่ม · แตะสั้นกับกดค้างเป็นคนละคำสั่ง จับเวลาตอนกดลง แล้วตัดสินตอนปล่อย ไม่ใช่ตอนกด |
คาบนี้มีตัวอย่างเจ็ดไฟล์ สามไฟล์เป็นเรื่องไมโครโฟน ซึ่งตอนนี้ใช้จาก Python ได้แล้วผ่านโมดูล mic ที่ฝังมากับเฟิร์มแวร์ ทั้งบนบอร์ดและในอีมูเลเตอร์ ดังนั้น การ์ดระดับเสียงวางในผังสี่การ์ดได้แล้ว ทีมที่อยากได้การ์ดที่ไม่ซ้ำกับใครเลือกใบนี้ได้ — สามไฟล์นั้นแบ่งกันคนละหน้าที่: 02 ใช้ level() · 03 ใช้ peak() · 07 ใช้ read() stats() lag() ครบทั้งสามชื่อที่เหลือ
ฝั่งระบบสมองกลฝังตัว
การจองทรัพยากรแบบคงที่ (ตาราง handle 64 ช่อง) และเหตุผลว่าทำไมระบบที่ต้องทำงานยาวถึงเลือกทางนี้ · การทำงานภายใต้งบที่นับได้ · การทดสอบความทนทานด้วย soak run · การอ่านอุปกรณ์แบบ synchronous ในลูปเดียว
ฝั่ง Python และวิทยาการคอมพิวเตอร์
try/except กันลูปตายเพราะเซนเซอร์ตัวเดียว · การเก็บ handle ของอ็อบเจกต์ไว้ในตัวแปรเพื่อสั่งงานทีหลัง · time.ticks_ms() / ticks_diff() กับการวัดเวลาแบบไม่ล้น · การแปลงค่าต่อเนื่องเป็นหมวด (องศา → ชื่อทิศ)
ฝั่งการออกแบบระบบ
การจัดกลุ่มข้อมูลตามคำถามที่มันตอบ · ลำดับความสำคัญทางสายตา · การใช้สีเป็นภาษาแทนการตกแต่ง · การเลือก widget ตามว่าใครเป็นคนเปลี่ยนค่า · การออกแบบภายใต้ข้อจำกัดที่แก้ไม่ได้
ข้อสุดท้ายคือของที่ใช้ได้แม้เปลี่ยนภาษา เปลี่ยนบอร์ด และเปลี่ยนอาชีพ
วันนี้เราได้:
ออกแบบผัง HMI บนกระดาษก่อนเขียนโค้ด · ประกอบ ui.Panel สี่ใบเข้ากับ Chart, Compass, Bar, Arc, Seg7 · ทำงานภายใต้งบ 32 widgets ที่ตั้งเอง (เพดานเฟิร์มแวร์ 64) โดยรู้ว่าทุกตัวไปอยู่ไหน · อ่านเซนเซอร์สี่ตัวแบบ sync ในลูปเดียวที่ 200 ms · ทดสอบความทนทานสิบนาทีและแยกอาการค้างออกจากอาการช้า
การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ถัดไป บันทึกลงใบงานข้อ 8
คาบหน้า: เราจะต่อบอร์ดเข้า WiFi แล้วเอา SSID, IP และค่า ping ขึ้นมาแสดงบนแดชบอร์ดที่สร้างวันนี้ — MVP ของคาบ 9 ระบุไว้ชัดว่าให้ ต่อยอดจากหน้าจอของคาบ 8 ไม่ใช่เริ่มใหม่
เก็บไฟล์วันนี้ให้ดีและอย่าลบ คาบ 9 ถึง 12 จะงอกออกจากไฟล์นี้ทั้งหมด
s08_dashboard.py↗ — ส่วนที่หนึ่ง: หัวไฟล์กับงบอ่านให้เข้าใจ แล้วพิมพ์เอง อย่าคัดลอกวาง
# งบ widget ของไฟล์นี้ = 32 (เพดานของเฟิร์มแวร์คือ 64 ห้ามเกิน)
# หัวเรื่อง 1 + ไฟค่าค้าง 1 + ป้ายค่าค้าง 1 + แถบสถานะ 1 = 4
# การ์ด IMU Panel + หัวข้อ + Chart + Label ค่า = 4
# การ์ดเข็มทิศ Panel + หัวข้อ + Compass + Label องศา + Label ทิศ = 5
# การ์ดสัมผัส Panel + หัวข้อ + ไฟ 2 ดวง + ป้าย 2 + Bar + Scale + % = 9
# การ์ด pot Panel + หัวข้อ + Arc + Seg7 + Label โวลต์ = 5
# แถบคำสั่ง ปุ่ม 2 + ป้ายเกณฑ์ + Spinbox + ไฟเตือน + ป้าย = 5
import ui
import sensors
import time
TEAM = "BentoBuilders"
UI_TEXT_MS = 1000 # ตัวเลขที่คนต้องอ่าน เขียนใหม่ไม่เกินวินาทีละครั้ง
STALE_MS = 3000 # อ่านไม่ได้ติดกันเกินเท่านี้ ถือว่าเลขบนจอเป็นของเก่า
ui.screen()
time.sleep_ms(200)
บล็อกนับ widget อยู่ในหัวไฟล์ ไม่ใช่ในสมุด เพราะโค้ดกับงบต้องเดินทางไปด้วยกัน คนที่เปิดไฟล์นี้ในอีกสองสัปดาห์ (ซึ่งมักคือตัวเราเอง) จะเห็นทันทีว่าเหลืองบเท่าไรก่อนจะเผลอเพิ่ม Label ตัวที่ 65
การนำเข้าโมดูลสามตัวนี้ครบพอดีสำหรับงานวันนี้ — ไม่มี math เพราะเราไม่ได้คำนวณอะไรที่ต้องใช้
เอกสารที่อยู่ไกลจากโค้ดจะเก่าเสมอ เอกสารที่อยู่บรรทัดบนโค้ดมีโอกาสถูกแก้ตาม
imu_panel = ui.Panel(x=24, y=100, w=368, h=136,
color=BG_CARD, min=COL_IMU, max=12, value=2)
imu_chart = ui.Chart(x=40, y=136, w=336, h=56, min=-150, max=150, color=COL_IMU)
sy = imu_chart.add_series(COL_STAT) # แกน Y เขียว
sz = imu_chart.add_series(0x448AFF) # แกน Z ฟ้า
...
comp_panel = ui.Panel(x=408, y=100, w=360, h=136,
color=BG_CARD, min=COL_COMP, max=12, value=2)
compass = ui.Compass(x=424, y=132, w=96, h=96, color=COL_COMP)
...
cap_panel = ui.Panel(x=24, y=252, w=320, h=136,
color=BG_CARD, min=COL_TOUCH, max=12, value=2)
cap_led0 = ui.Led(x=40, y=288, w=48, h=48, color=COL_TOUCH, value=0)
cap_bar = ui.Bar(x=40, y=348, w=288, h=16, min=0, max=100, value=0, color=COL_TOUCH)
...
pot_panel = ui.Panel(x=360, y=252, w=408, h=136,
color=BG_CARD, min=COL_POT, max=12, value=2)
pot_arc = ui.Arc(x=376, y=288, w=88, h=88, min=0, max=100, value=0)
spin_thr = ui.Spinbox(x=600, y=288, w=88, h=88, color=COL_WHITE,
min=0, max=100, value=80)
led_alarm = ui.Led(x=704, y=288, w=48, h=48, color=COL_ALERT, value=0)
สูตรเดียวกันทั้งหน้า: ขอบซ้าย 24 · ช่องไฟระหว่างการ์ด 16 · การ์ดสูง 136 ทั้งสองแถว · ปุ่มสั่งงานอยู่ใน แถบหัว (สูง 88 px) ไม่ใช่แถบล่าง เพราะการ์ดสองแถวกินจนหมด 398 แล้ว · ของที่เปลี่ยนจากรุ่นก่อน — ป้าย B0 ON ที่เปลี่ยนสีถูกแทนด้วย ui.Led สองดวง (ป้ายที่บอกสถานะด้วยสีอย่างเดียว ไม่ผ่านการทดสอบขาวดำ) · เกณฑ์เตือน spin_thr + led_alarm อยู่ ในการ์ดลูกบิด เพราะเป็นเกณฑ์ของลูกบิด — กลุ่มนี้เคยอยู่ที่ y=404–612 บนจอสูง 398 คือนอกจอทั้งแถบ กดไม่ได้และไม่มีใครเห็นว่าหายไป · imu_chart กับ compass ไม่ใช้ COL_ALERT อีกแล้ว สีเตือนต้องใช้กับการเตือนเท่านั้น
ถ้าต้องขยับการ์ดใบหนึ่ง ต้องขยับเลขทั้งแถว — จดสูตรไว้ในใบงาน จะแก้ได้เร็วกว่าเดาทีละตัว
try:
while True:
t0 = time.ticks_ms()
ok = True
if running:
try:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
except OSError:
ok = False # คงค่าเดิมไว้ ไม่เขียนศูนย์ทับ
imu_chart.set_next(0, int(ax * 10))
...
if ok:
t_good = time.ticks_ms()
stale = time.ticks_diff(time.ticks_ms(), t_good) >= STALE_MS
if stale != stale_shown: # เขียนเฉพาะตอนเปลี่ยน
stale_shown = stale
led_stale.value(1 if stale else 0)
if time.ticks_diff(time.ticks_ms(), last_text) >= UI_TEXT_MS:
last_text = time.ticks_ms()
pot_seg7.text("{:.1f}".format(pct))
head.text("รอบที่ {} | loop {} ms".format(
rounds, time.ticks_diff(time.ticks_ms(), t0)))
rounds += 1
for ev in ui.poll():
... # ปุ่มเดินหน้า/หยุดภาพ และ Spinbox เกณฑ์
time.sleep_ms(200)
except KeyboardInterrupt:
ui.clear()
try/except ครอบการอ่านเซนเซอร์ ทีละตัว ไม่ใช่ครอบทั้งลูป — เข็มทิศตัวเดียวมีปัญหา อีกสามการ์ดยังต้องทำงานต่อ · ticks_diff() แทนการลบตรง ๆ เพราะ ticks_ms() วนกลับเป็นศูนย์ในวันที่รันยาว · except ไม่เขียนศูนย์ทับค่าเดิม — ศูนย์คือตัวเลขที่หน้าตาเหมือนค่าที่วัดมาจริง รุ่นนี้คงค่าล่าสุดไว้แล้วจุดไฟ "ค่าค้าง" ตอบคำถามที่แดชบอร์ดทุกใบต้องตอบ: เลขที่เห็นอยู่ตอนนี้ ใช่ค่าปัจจุบันไหม · ตัวเลขขยับไม่เกินวินาทีละครั้ง ทั้งที่ลูปเดินทุก 200 ms — กราฟ แถบ เข็ม และไฟขยับที่ 200 ms ได้ เพราะตาอ่านรูปทรง แต่ตัวเลขที่กระพริบห้าครั้งต่อวินาทีไม่มีใครอ่านทันการเลือกขอบเขตของ
tryคือการตัดสินใจว่า "อะไรพังได้โดยที่ระบบยังใช้งานได้อยู่"
ท่า 1 เตรียมจอ ชุดสี เซนเซอร์ มาก่อน เพราะทั้งสามอย่างคือรากที่ท่าอื่นยืนอยู่บน ถ้า ui.screen() ไม่ทำงาน หรือเซนเซอร์ยังไม่ตื่น ที่เหลือไม่มีประโยชน์จะเขียนต่อ
ท่า 2 การ์ด IMU มาเป็นใบแรกเพราะมันซับซ้อนที่สุด (Panel + Chart + series สามเส้น) ถ้าใบยากที่สุดผ่านแล้ว ใบที่เหลือคือการทำซ้ำแบบเดียวกัน — ทำของยากตอนที่ยังมีแรงและยังมีเวลาเหลือ
ท่า 3–4 การ์ดที่เหลือ เรียงตามผังจากซ้ายไปขวา บนลงล่าง ตรงกับที่ตาคนอ่าน ทำให้เวลากลับมาแก้ หาตำแหน่งในไฟล์ได้จากตำแหน่งบนจอ
ท่า 5 แถบคำสั่ง มาก่อนลูป เพราะปุ่มกับ Spinbox ต้องมีตัวตนอยู่ก่อน ลูปถึงจะอ้าง btn_run.id() ได้ และเพราะมันคือส่วนที่ทำให้แดชบอร์ดใบนี้ เป็นแผงควบคุม ไม่ใช่โปสเตอร์ที่ตัวเลขขยับได้ แดชบอร์ดที่ผู้ใช้แตะอะไรไม่ได้เลย ตอบได้แค่ "ตอนนี้เป็นยังไง" แต่ตอบไม่ได้ว่า "แล้วจะให้ทำอะไรต่อ"
ท่า 6 ลูปหลัก มาสุดท้ายเสมอ เพราะลูปคือที่ที่ทุกอย่างมาเจอกัน ถ้าเขียนลูปก่อนสร้าง widget โปรแกรมจะพังที่ชื่อตัวแปรที่ยังไม่มี
หลักการเดียวกับที่ใช้มาตั้งแต่คาบ 1: สร้างของนิ่งให้ครบก่อน แล้วค่อยใส่ของที่เคลื่อนไหว ของนิ่งพังจะเห็นทันทีจากตา ของเคลื่อนไหวพังต้องนั่งดูสักพักถึงจะรู้
ลำดับที่ดีคือลำดับที่ทำให้ "รู้ว่าพังตรงไหน" ได้เร็วที่สุด ไม่ใช่ลำดับที่พิมพ์แล้วลื่นที่สุด
คำถามคิดต่อ: ถ้าต้องเพิ่มการ์ดสถานะเครือข่ายในคาบหน้า จะตัดอะไรออกหรือใช้งบที่เหลือ · การ์ดใบไหนที่คนดูจริงจะมองบ่อยที่สุด และมันควรอยู่ตำแหน่งนั้นไหม

เซนเซอร์ที่การ์ดสองใบล่างต้องใช้ — Eva Kit ไม่มีบนบอร์ด ยกมาเทียบให้เห็นว่ามันวัดยังไง · TESAIoT Dev Kit มีทั้งสองตัว (DPS368 ความดัน · SHT40 อุณหภูมิ/ความชื้น เรียกผ่าน sensors.dps368 / sensors.sht40) ทีม Dev Kit จึงทำการ์ดพวกนี้ด้วยค่าจริงได้
Barometric pressure sensor: working principle — Bosch Sensortec — 0:53 · Understanding Temperature Sensor Technology — RealPars — 8:59 (ช่วง thermistor เริ่ม 5:26)
ทั้งสี่แบบใช้หลักการเดียวกับที่เราทำวันนี้ทุกข้อ ต่างกันแค่ว่าเบื้องหลังการ์ดเป็นเซนเซอร์อะไร
ข้อ 1 · การ์ดใบที่ห้า: สถานะระบบ
เพิ่มการ์ดที่รายงานสุขภาพของโปรแกรมเอง — เวลาที่รันมาแล้ว (นาที), จำนวนรอบ, และ loop ms สูงสุดที่เคยเจอ ต้องอยู่ในงบ 32 ที่ตั้งไว้ให้ได้ (ไม่ใช่ขยับไปหาเพดาน 64) บอกมาด้วยว่าตัดอะไรออกหรือใช้งบที่เหลือ
ข้อ 2 · โซนสีตามเกณฑ์
ทำให้ค่าบนการ์ดเปลี่ยนสีเองตามเกณฑ์ที่ทีมตั้ง เช่น ขนาดความเร่งรวมเกิน 12 m/s² ให้ตัวเลขเป็นแดง อยู่ระหว่าง 11–12 เป็นเหลือง ต่ำกว่านั้นเป็นเขียว เขียนในใบงานด้วยว่าทำไมถึงเลือกเกณฑ์นี้
ข้อ 3 · สลับหน้าด้วย .show() / .hide()
สลับหน้าเราทำเองได้ (จริง ๆ ui.Tabview ก็มี — คาบ 4 ใช้ไปแล้ว — แต่ท่าซ่อน/โชว์การ์ดเบากว่าเมื่อการ์ดเดิมอยู่ครบ) — เพิ่มปุ่มสองปุ่มสลับระหว่างหน้า "ภาพรวม" กับหน้า "IMU เต็มจอ" โดยซ่อนการ์ดที่ไม่ได้ใช้แทนการลบทิ้ง วิธีนี้ประหยัดงบ widget อย่างไรเมื่อเทียบกับการสร้างใหม่ทุกครั้ง
ข้อ 4 · รายงานผล soak run
รันยาว 10 นาทีสองรอบ รอบแรกที่ sleep_ms(200) รอบสองที่ sleep_ms(80) จดเลขรอบทุกสองนาทีทั้งสองรอบ แล้วเขียนสรุปครึ่งหน้าว่าอะไรเปลี่ยนไปบ้าง และทีมจะเลือก cadence เท่าไรถ้าต้องส่งงานจริง
เขียนคำตอบลงใบงานข้อ 8 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า


เอกสารบอร์ดและเซนเซอร์ (แหล่งปฐมภูมิ)
bst-bmm350-ds001.pdf บน bosch-sensortec.comซอฟต์แวร์
วิดีโอที่ตรวจแล้วว่าเปิดได้ (บันทึกใน _BUILD/MEDIA_PLAN.md)
RLQGZl0lpjQ Working principle of an accelerometer — Bosch Sensortec6BS6aQBaMhU Projected Capacitive Touch Technology - How It Works — Zytronic Displays0bw5UJQxkhA Barometric pressure sensor: working principle — Bosch SensortecJ2ulD-bPTS4 Understanding Temperature Sensor Technology — RealParsเพดาน 64 widgets · ความหมาย kwarg ของ
ui.Panel· ขนาดจอ 792x398 — มาจากซอร์สเฟิร์มแวร์เอง ดู path ในสไลด์ผู้สอน
ภาพประกอบ
b_sliding_window_smoothing_animation.gif / Wikimedia Commons — CC0 1.0
KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw/sim) ซึ่งรัน ipc_ui.c กับ ui_widget_mgr.c ตัวจริงเดียวกับบอร์ด บนพื้นที่วาด 800x398 · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ examples/s08/08_win_titled_card.py↗ · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ดui.Win คือกรอบที่มีแถบหัวเรื่องมาให้แล้ว .content() คืนกล่องข้างในที่เราเอา widget ไปใส่ด้วย parent=
| แฮนเดิล | หัวเรื่อง | ราคาที่ซ่อนอยู่ | |
|---|---|---|---|
ui.Win + .content() |
2 | เฟิร์มแวร์จัดให้ | แถบหัวกินความสูงราว 60 px จากกรอบ |
ui.Panel + ui.Label |
2 | เราจัดเอง | ไม่มี แต่ต้องนับพิกัดเอง |
ราคาเท่ากัน ต่างกันที่ ใครเป็นคนจัดตำแหน่งหัวเรื่อง — และที่ Win หักความสูงไปโดยไม่บอก
text= ใช้ได้ตอนสร้างเท่านั้น ในภาพสั่งเปลี่ยนหัวเรื่องไปแล้ว 18 ครั้ง หัวยังเหมือนเดิม
ตั้งความสูงเท่าการ์ดธรรมดาแล้วเนื้อในจะถูกตัด พร้อมแถบเลื่อนโผล่มาเงียบ ๆ — เผื่อความสูงให้แถบหัวเสมอ
fit-css
VIDEO-SLOT: คลิป 45-60 วินาที ถ่ายจอบอร์ดขณะเปิดเมนู Sensor Dashboard ของเฟิร์มแวร์ — ถ่ายทั้ง Eva Kit และ Dev Kit เพราะการ์ดไม่เหมือนกัน — ให้เห็นการ์ดพร้อมกัน เอียงบอร์ดแล้วค่า IMU วิ่ง หมุนบอร์ดแล้วเข็มทิศหมุน แตะ CapSense แล้วสถานะในการ์ด Controls เปลี่ยน ลากนิ้วบนแถบเลื่อนแล้ว slider ขยับ (หน้าเฟิร์มแวร์ไม่มีการ์ดลูกบิด)
VIDEO-SLOT: คลิป 60-90 วินาที ถ่ายจอบอร์ดตอนแดชบอร์ดสี่การ์ดรันครบ — เขย่าบอร์ดให้กราฟวิ่ง หมุนบอร์ดให้เข็มทิศและตัวอักษรทิศเปลี่ยน แตะ CapSense ให้ไฟดวงนั้นติด หมุนลูกบิดให้ Arc กับ Seg7 ขยับพร้อมกัน ปิดท้ายด้วยการซูมเข้าแถบสถานะให้เห็นเลขรอบเดินและ loop ms คงที่หลังรันมาสิบนาที
โน้ตผู้สอน: อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของคาบ 8 เพราะเป็นเอกสารอ้างอิงของตารางงบข้อ 4.1 มากกว่าจะเป็นบทเรียน: examples/s04/06_layout_budget.py นับ widget ที่สร้างไปแล้วและจับ RuntimeError ตอนชนเพดาน 64 เปิดตอนงบเริ่มตึงแล้วอยากรู้ว่าเหลือเท่าไรจริง ๆ