คาบ 8 — Mini-HMI Dashboard

ประกอบทุกอย่างที่เรียนมาให้เป็นหน้าจอเดียว ภายใต้งบ 32 widgets ที่ตั้งเอง

คาถาประจำคาบ: ออกแบบบนกระดาษก่อน แล้วค่อยพิมพ์ — ข้อจำกัดคือส่วนหนึ่งของโจทย์

ดูของจริงก่อน — แดชบอร์ดเต็มรูปแบบที่บอร์ดทำได้

ผังของ lvgl_dashboards/12_dash_full_eva.py — ไม่ใช่หน้าเฟิร์มแวร์ 792 x 398 IMU · BMI270 Compass · BMM350 137 deg SE CapSense B0 --- B1 --- Potentiometer 48 % Gyro X +0.4 Y -1.2 deg/s สถานะโปรแกรม รอบที่ 1842 loop 3 ms

ผู้สอนเปิดเมนู Sensor Dashboard ของเฟิร์มแวร์บนบอร์ดหน้าห้อง แล้วปล่อยให้มันวิ่ง

หน้าจอเดียว การ์ดหลายใบ เซนเซอร์หลายตัวอัปเดตพร้อมกัน ไม่มีเมนูให้เข้า มองแล้วรู้สถานะทั้งระบบภายในสองวินาที — นั่นคือสิ่งที่โรงงานเรียกว่า HMI (Human-Machine Interface) หน้าจอที่คนยืนดูแล้วตัดสินใจได้ · วันนี้เราจะสร้างของแบบนี้เอง ด้วยงบ 32 widgets ที่ตั้งเอง (เพดานเฟิร์มแวร์ 64) และไฟล์ Python ไฟล์เดียว

ภาพจำลองข้างบนคือผังของ lvgl_dashboards/12_dash_full_eva.py (ตัวอย่าง MicroPython ที่มากับเฟิร์มแวร์ หัวไฟล์นับไว้ 23 widgets — ไม่ใช่ไฟล์ในหลักสูตรนี้) ไม่ใช่หน้า Sensor Dashboard ของเฟิร์มแวร์ — หน้าเฟิร์มแวร์บน Eva Kit มีการ์ด BMI270 · Controls (btn0/btn1/slider) · กราฟการเคลื่อนไหว · เข็มทิศ · จอยสติ๊ก และ ไม่มีการ์ดลูกบิด · บน TESAIoT Dev Kit หน้าเดียวกันมีการ์ดเพิ่มที่ Eva ไม่มี คือ DPS368 (ความดัน) SHT40 (อุณหภูมิ/ความชื้น) และเรดาร์ เพราะเฟิร์มแวร์ประกอบการ์ดตามชิปที่บอร์ดมี (page_dashboard.c ครอบด้วยธง BSP_HAS_*) ดูของจริงจากบอร์ดของทีม

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

คำถาม คำตอบของคาบนี้ อยู่ช่วงไหน
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 เราทำกราฟหนึ่งใบให้ลื่น · คาบนี้ต้องทำให้ของทั้งจอลื่นพร้อมกัน ภายใต้งบที่นับได้

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

  1. ออกแบบผัง HMI บนกระดาษได้ก่อนเขียนโค้ด — วางการ์ด คำนวณพิกัด และ นับ widget ให้ครบก่อนพิมพ์
  2. อธิบายหลักการ HMI ที่ใช้ในแผงควบคุมจริงได้: การจัดกลุ่ม ลำดับสายตา และความหมายของสี
  3. ประกอบ ui.Panel สี่ใบเข้ากับ Chart, Compass, Bar, Arc, Seg7 ให้เป็นหน้าจอเดียวที่อ่านรู้เรื่อง
  4. ทดสอบความทนทานด้วยการรันต่อเนื่อง 10 นาที และแยกให้ออกว่า "ค้าง" กับ "ช้า" ต่างกันอย่างไร

ปลายทางของวันนี้: จอบอร์ดขึ้นแดชบอร์ดสี่การ์ดของทีมเรา รันยาวสิบนาทีโดยไม่มีอะไรสะดุด

คาบนี้คือ capstone ครึ่งทาง — ไม่มีของใหม่มาก แต่ต้องเอาของเก่าทั้งหมดมาอยู่ร่วมจอเดียวกันให้ได้

ปลายทางของคาบนี้ — สี่การ์ดในจอเดียว

หน้าจอจริงจากการรันโค้ดเฉลยบน BENTO Emulator ที่ 800x480 เท่าจอของ Eva Kit และ Dev Kit — ไม่ใช่ภาพวาด ไม่ใช่ mock-up และไม่ใช่ภาพถ่ายจากบอร์ด
  • การ์ดสี่ใบเต็มจอ: IMU chart, Compass 013 deg, CapSense และ Potentiometer
  • แต่ละใบคือ Panel หนึ่งใบที่ให้สีขอบต่างกัน ใช้แยกกลุ่มข้อมูลด้วยสายตาก่อนอ่านตัวหนังสือ
  • แถบล่างบอกชื่อทีมกับ "รอบที่ 17" ส่วนตัวเลข loop ทางขวาถูกปุ่มลอยมุมขวาบังไปบางส่วน — เป็นข้อจำกัดพื้นที่ที่ต้องออกแบบเผื่อ

งบ widget มีจำกัด การจัดวางจึงเป็นการตัดสินใจ ไม่ใช่การตกแต่ง

ทบทวน — สามชิ้นส่วนที่เราสร้างไว้แล้ว

คาบ 5 · Arc Seg7 Bar pot และ CapSense คาบ 6 · motion() 6 แกน อ่านทีเดียวได้ครบ คาบ 7 · Chart หลายเส้น cadence 200 ms หน้าจอเดียว · การ์ดสี่ใบ · 23 widgets IMU + Chart ของเดิมจากคาบ 7 Compass ของใหม่วันนี้ CapSense ของเดิมจากคาบ 5 Pot ของเดิมจากคาบ 5
จากคาบ สิ่งที่ทำได้แล้ว วันนี้กลายเป็น
คาบ 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()

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

เนื้อหาใหม่น้อย แต่ความยากขึ้นชัด เพราะครั้งนี้ทุกชิ้นต้องทำงานพร้อมกัน

ทบทวน (ต่อ) — ของสองอย่างที่ต้องเห็นตอนมันเคลื่อน: สนามแม่เหล็ก และนิ้วบนกระจก

ที่มา: Peo / Hike395 / Wikimedia Commons — CC BY-SA 3.0 · สนามแม่เหล็กที่เปลี่ยนทิศแล้วแรงดันที่ได้เปลี่ยนตาม — ต้องเห็นทั้งสี่กรณีสลับกันถึงจะเข้าใจว่าทำไมค่าสามแกนถึงพลิกเครื่องหมายเมื่อหมุนบอร์ด

ดูเพิ่ม (7 นาที): Projected Capacitive Touch Technology - How It Works — นิ้ว "ขโมย" เส้นสนามไฟฟ้า คือค่าที่ capsense.read() คืนมา

HMI คืออะไร และทำไมต้องจัดของเป็นการ์ด

ตัวเลข 15 ตัว ไม่มีกรอบ 12.4 0.98 137 48 3.1 9.81 -1.2 0 62 1.64 204 0.4 17 -- 2.99 ต้องไล่อ่านป้ายทีละตัว กว่าจะเจอค่าที่ต้องการ การ์ดสี่ใบ หนึ่งใบหนึ่งคำถาม การเคลื่อนไหว เป็นยังไง หันหน้า ไปทางไหน มีใครแตะ อยู่ไหม ลูกบิดตั้งไว้ เท่าไร

แผงควบคุมในโรงงานไม่ได้ออกแบบให้ "สวย" มันออกแบบให้ คนที่เหนื่อยและรีบ อ่านถูกในครั้งแรก

หลักข้อแรกคือการจัดกลุ่ม: ค่าที่มาจากแหล่งเดียวกันหรือใช้ตัดสินใจเรื่องเดียวกัน ต้องอยู่ในกรอบเดียวกัน

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

การ์ดหนึ่งใบตอบคำถามหนึ่งคำถาม: "การเคลื่อนไหวเป็นยังไง" · "หันหน้าไปทางไหน" · "มีใครแตะอยู่ไหม" · "ลูกบิดตั้งไว้เท่าไร"

ถ้าตอบไม่ได้ว่าการ์ดใบนี้ตอบคำถามอะไร แสดงว่ายังไม่ควรมีการ์ดใบนี้

ลำดับสายตา — อะไรต้องอ่านออกจากอีกฝั่งห้อง

Compass - BMM350 137 deg SE อัปเดตล่าสุด 0.2 วินาทีที่แล้ว ชั้น 3 · ชื่อการ์ด — font 20 สีจาง ชั้น 1 · ค่าหลัก — font 28 อ่านจาก 3 เมตร ชั้น 2 · ค่าประกอบ — font 24 อ่านตอนเดินเข้ามา ชั้น 3 · รายละเอียด — font 16-18

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

สามชั้นที่ใช้จริงในงาน HMI

  1. ชั้นที่ต้องอ่านจากระยะสองสามเมตร — ตัวเลขหลักของการ์ด ใช้ฟอนต์ 24–28 สีขาวสว่างบนพื้นเข้ม
  2. ชั้นที่อ่านตอนเดินเข้ามาใกล้ — ค่ารายละเอียด กราฟ แถบ ใช้ฟอนต์ปกติ
  3. ชั้นที่อ่านเฉพาะตอนหา — ชื่อการ์ด หน่วย ป้ายกำกับ ใช้สีจางลง

ใน ui เรามีเครื่องมือคุมสามชั้นนี้อยู่แค่สองอย่าง: value= ของ Label (ขนาดฟอนต์ 14/16/20/24/28) และ color= เท่านั้น จึงต้องใช้ให้ตรงเป้า อย่าใส่ 28 ให้ทุกตัวเพราะ "ใหญ่แล้วดูดี" — ถ้าทุกอย่างเด่น แปลว่าไม่มีอะไรเด่น

ในโค้ดวันนี้ ตัวเลของศาของเข็มทิศได้ 28 px ส่วนชื่อการ์ดได้ 20 px และหน่วยได้สีเทา นั่นคือการตัดสินใจ ไม่ใช่ความบังเอิญ

ภาพ: hanmaili / Wikimedia Commons — CC0 1.0 · แผงเฟดเดอร์ของมิกเซอร์จริง — ค่าหลายสิบค่าที่อ่านได้ในสายตาเดียวเพราะตำแหน่งเรียงกัน ไม่ใช่เพราะตัวเลขใหญ่

ขนาดฟอนต์คือการประกาศว่า "ของชิ้นนี้สำคัญกว่าชิ้นนั้น" — ประกาศให้ตรงกับความจริง

สีมีความหมาย ไม่ใช่ของตกแต่ง

เขียว · ปกติ 9.8 อยู่ในเกณฑ์ ไม่ต้องทำอะไร เหลือง · เฝ้าดู 11.4 เริ่มออกนอกช่วงที่คุ้น กลับมาดูอีกที แดง · ต้องลงมือ 14.9 เกินเกณฑ์แล้ว หยุดหรือแก้ เดี๋ยวนี้ ห้ามใช้แดงกับของที่ปกติ — วันที่เกิดเรื่องจริงจะไม่มีใครสังเกตเห็น

ในแผงควบคุมจริง สีสามสีนี้ถูกจองไว้แล้ว และคนทั้งอุตสาหกรรมอ่านมันตรงกัน

สี ความหมาย คนดูควรทำอะไร
เขียว ปกติ อยู่ในเกณฑ์ ไม่ต้องทำอะไร
เหลือง/อำพัน เฝ้าดู เริ่มออกนอกช่วงที่คุ้นเคย กลับมาดูอีกที
แดง ต้องลงมือ หยุด/แก้ เดี๋ยวนี้

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

ภาพ: User:Mattes / Wikimedia Commons — สาธารณสมบัติ · เสาไฟสถานะจริงบนเครื่องจักร แดง-เหลือง-เขียวเท่านั้น — ยืนยันว่าชุดสีที่อ่านออกจากระยะไกลมีไม่กี่สี และความหมายถูกล็อกไว้แล้ว

จานสีของเฉลย — สีสถานะสามสี กับสีประจำเซนเซอร์สี่สี

สีที่เหลือ (ม่วง · เขียวน้ำทะเล · ม่วงกล้วยไม้ · ฟ้า — จานสีเส้นข้อมูลของ 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 และอย่าสุ่มเลขสีเองเด็ดขาด

อัตราอัปเดต กับ ความอ่านออก — สองอย่างนี้ตีกัน

อัปเดตทุก 20 ms · 50 ครั้งต่อวินาที 47.2 51.8 44.6 49.1 ตาคนอ่านไม่ทัน อัปเดตทุก 200 ms · 5 ครั้งต่อวินาที 47.2 48.1 48.6 48.4 อ่านทัน เห็นทิศทาง ฝั่งเครื่อง — คิววาดของ CM55 ลูปเร็วเกิน คิวเต็ม เฟรมหายเงียบ ๆ ไม่มี error ที่ 200 ms คิวว่างเสมอ

สัญชาตญาณบอกว่ายิ่งอัปเดตถี่ยิ่งดี ในงาน HMI มันไม่จริง

ตัวเลขที่เปลี่ยนทุก 20 ms คือตัวเลขที่ มนุษย์อ่านไม่ทัน สายตาเห็นเป็นเลขเบลอ ๆ ที่กระพริบ ส่วนกราฟที่เลื่อนเร็วเกินก็ดูไม่ออกว่าแนวโน้มขึ้นหรือลง

ที่ 200 ms คนอ่านตัวเลขทันพอดี (ห้าครั้งต่อวินาที) กราฟเลื่อนแบบเห็นทิศทาง และ CM55 มีเวลาวาดครบทุกเฟรม

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

จำง่าย ๆ: แดชบอร์ดหนัก 200 ms · หน้าเบา ๆ ที่มีไม่กี่ widget อย่างต่ำ 50 ms

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

อัตราอัปเดต กับ ความอ่านออก (ต่อ) — หน้าตาของปัญหา และราคาของการอ่านออก

ซ้าย: Sehri M. et al., Data in Brief (2023) — CC BY 4.0 · ข้อมูลสั่นสะเทือนดิบอัตราสูงจากเครื่องจักรจริง วาดลงจอทุกจุดแล้วคนอ่านไม่ออก นี่คือหน้าตาของปัญหาที่สไลด์นี้กำลังพูดถึง · ขวา: Kisiel M. et al., Sensors 26(3):876 (2026) — CC BY 4.0 · ค่าความเร่งจากการเดินจริง วัดที่แขนเทียบกับที่ข้อเท้าพร้อมสเปกตรัมของทั้งสองจุด จุดติดตั้งเปลี่ยนรูปสัญญาณทั้งชุด ไม่ใช่แค่ขนาด

ภาพ: b_sliding_window_smoothing_animation.gif / Wikimedia Commons — CC0 1.0 · หน้าต่างเฉลี่ยที่เลื่อนไปบนข้อมูลจริง — ให้เห็นว่าการทำให้อ่านออกคือการยอมช้าลง ไม่ใช่การได้ของฟรี

เข้าใจฮาร์ดแวร์ · จอกว้าง 792 สูง 398 และเลขพิกัดที่เราต้องคำนวณเอง

ui ไม่มีระบบ layout อัตโนมัติแบบเว็บ (ไม่มี grid ไม่มี flex) ทุก widget ต้องบอกพิกัดเอง เราจึงต้องบวกลบเลขบนกระดาษก่อน

หัวเรื่อง/แถบบน y=6 การ์ด 1 · IMU + Chart x=8 y=34 w=386 h=176 Chart อยู่ใน x=18 y=66 w=366 h=102 4 widgets การ์ด 2 · Compass x=402 y=34 w=382 h=176 Compass x=420 y=68 w=126 5 widgets การ์ด 3 · CapSense x=8 y=218 w=386 h=140 Bar x=22 y=300 w=350 h=20 6 widgets การ์ด 4 · Pot x=402 y=218 w=382 h=140 Arc w=100 · Seg7 w=150 5 widgets แถบสถานะ y=370 (รอบที่ N | loop ms) ช่องไฟ 8 px รอบขอบและระหว่างการ์ด

เลขที่ต้องตรวจให้ตรงเสมอ: x + w ต้องไม่เกิน 792 และ y + h ต้องไม่เกิน 398 (การ์ด 2: 402+382 = 784 เหลือขอบขวา 8 px พอดี)

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

ui.Panel — การ์ดหนึ่งใบ กับพารามิเตอร์ที่ชื่อไม่ตรงความหมาย

ui.Panel min = สีขอบ (ไม่ใช่ค่าต่ำสุด) color = สีพื้นของการ์ด max = รัศมีมุมโค้ง value = ความหนาขอบ ชื่อ kwarg เดียวกัน เปลี่ยนความหมายไปตามชนิดของ widget

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 ตัวไหนแปลว่าอะไร ให้เปิดตารางนี้ อย่าเดาจากชื่อ

งบ widget — นับบนกระดาษให้ครบก่อนพิมพ์โค้ดบรรทัดแรก

งบ 32 ช่องที่เราตั้งเอง (เพดานเฟิร์มแวร์ 64) — จองไป 23 เหลือ 9 หัวเรื่อง + สถานะ 3 IMU 4 Compass 5 CapSense 6 Pot 5 ว่าง 9 ช่อง — เผื่อไว้ให้ทีมต่อยอด รวม 23 / 32

เฟิร์มแวร์รับได้ 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 พอดี

CM33 MicroPython ui.Label(...) ตาราง handle 64 ช่อง (วาดเฉพาะ 32 ช่องที่เป็นงบของคาบนี้) ขนาดคงที่ ไม่ขอหน่วยความจำเพิ่มตอนรัน CM55 + LVGL วาดของจริงบนจอ อ่านจากตารางนี้ widget ตัวที่ 65 ไม่มีช่องให้ลง — หายเงียบ ๆ ไม่มี error จองล่วงหน้าในขนาดที่รู้แน่ ดีกว่ายืดหยุ่นแล้วพังตอนทำงาน

ตัวเลข 64 ไม่ได้มาจากความสวยงามของเลขยกกำลังสอง มันมาจากข้อจำกัดจริงของการคุยข้ามคอร์ — และเท่ากันทั้ง Eva Kit และ Dev Kit เพราะสองบอร์ดใช้ ipc_ui ชุดเดียวกัน

โค้ด Python ของเราอยู่บน CM33 ส่วน widget จริง ๆ ถูกสร้างและวาดโดย LVGL บน CM55 สองฝั่งนี้คุยกันผ่าน IPC ซึ่งมีพื้นที่หน่วยความจำร่วมขนาดคงที่ ทุก widget ที่สร้างจะกิน "ช่อง" ในตารางอ้างอิงฝั่ง CM55 หนึ่งช่อง และตารางนั้นถูกจองขนาดไว้ตายตัวตั้งแต่ตอนคอมไพล์ — จองแบบไม่ต้องขอหน่วยความจำเพิ่มตอนรัน เพราะการขอหน่วยความจำระหว่างวาดจอคือทางลัดสู่การค้าง

นี่คือแบบแผนที่เจอได้ทั่วไปในงาน embedded: จองล่วงหน้าในขนาดที่รู้แน่ ดีกว่ายืดหยุ่นแล้วเสี่ยงพังตอนทำงาน ระบบที่ต้องทำงานยาว ๆ โดยไม่มีคนดูแล เลือกทางแรกเสมอ

เชื่อมกับวันนี้: ตอนทีมนั่งเถียงกันว่าจะตัด Label ตัวไหนออกเพื่อให้พองบ 32 ที่ตั้งไว้ — นั่นคือการทำงานภายใต้ข้อจำกัดของหน่วยความจำจริง แบบเดียวกับที่วิศวกรออกแบบ HMI ในเครื่องจักรทำอยู่ทุกวัน และเป็นเหตุผลที่ MVP ของวันนี้วัดกันที่ "รันสิบนาทีไม่ค้าง" ไม่ใช่ "มีของบนจอเยอะที่สุด"

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

70% — เฟิร์มแวร์ทำให้แล้ว วาดทุก widget ด้วย LVGL · จัดคิว IPC ข้ามคอร์ อ่านเซนเซอร์สี่ตัว · แปลงสนามแม่เหล็กเป็นองศา 30% — งานของเรา อะไรอยู่ด้วยกัน อะไรต้องเด่น · สีแปลว่าอะไร ถี่แค่ไหน · งบให้ใคร

สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
วาดทุก widget ด้วย LVGL, จัดคิว IPC ข้ามคอร์, อ่านค่าจากเซนเซอร์ทั้งสี่ตัว, แปลงสนามแม่เหล็กเป็นองศา, จัดการ touch

สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่า ค่าไหนควรอยู่ด้วยกัน · ค่าไหนต้องเด่น · สีอะไรแปลว่าอะไร · อัปเดตถี่แค่ไหน · และงบที่มีจะแบ่งให้ใคร

ห้าข้อนี้ไม่มีข้อไหนเป็นเรื่องไวยากรณ์ภาษา Python เลย มันคืองานออกแบบล้วน ๆ ซึ่งเป็นงานที่ยังไงเครื่องก็ทำแทนเราไม่ได้

โค้ดวันนี้ยาวขึ้นก็จริง แต่ส่วนที่ยากคือตอนตัดสินใจก่อนพิมพ์ ไม่ใช่ตอนพิมพ์

แกะโค้ดจริง — ท่าที่ 1 เตรียมจอ ชุดสี และเซนเซอร์

ลำดับเวลาตอนเปิดโปรแกรม — ไม่มี init ให้เรียก มีแต่การรอ ui.screen() ล้าง widget เดิม sleep_ms(200) ให้ CM55 ตามทัน motion() ใน try อ่านครั้งแรก = การรอ เข้าลูปหลัก ครั้งต่อไปรอไม่เกิน 1 วิ Eva: sensors.init() = OSError · Dev Kit: ผ่านแต่ไม่ต้องเรียก เฟิร์มแวร์ปลุกเซนเซอร์ให้แล้ว
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 เดิมทั้งหมด ไม่เรียกแล้วของจากสคริปต์ก่อนหน้าจะกินงบตั้งแต่ยังไม่เริ่ม

แกะโค้ดจริง — ท่าที่ 2 การ์ด IMU พร้อมกราฟสามแกน

ความเร่ง BMI270 (m/s2) X +0.2 Y -0.4 Z +9.8 Chart x=40 y=136 w=336 h=56 แกน -150 ถึง 150 คือ -15.0 ถึง +15.0 ม่วง = series แรก (color=) เขียว = add_series() ฟ้า = add_series() คูณ 10 ก่อนป้อน ไม่งั้นเป็นขั้นบันได Label ค่า y=200
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)

ซ้าย: Rodriguez V.H. et al., Sensors 22(20):7690 (2022) — CC BY 4.0 · ภาพถ่ายการติดตั้งเซนเซอร์จริงบนตัวคน พร้อมแกน roll/pitch/yaw กำกับ อธิบายว่าทำไมกราฟสามเส้นถึงเปลี่ยนพร้อมกันเมื่อเอียงบอร์ดเพียงแกนเดียว · ขวา: Vkidambi / Wikimedia Commons — CC BY-SA 4.0 · แรงโน้มถ่วงที่มองจากในลิฟต์ที่กำลังเร่ง เหตุผลที่ accelerometer วางนิ่ง ๆ แล้วยังอ่านได้ 9.8

กราฟบอกแนวโน้ม ตัวเลขบอกค่าปัจจุบัน — ใส่คู่กันเสมอ อย่างละครึ่งไม่พอสำหรับคนตัดสินใจ

แกะโค้ดจริง — ท่าที่ 3 การ์ดเข็มทิศ

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

บน: NASA/USGS (สาธารณสมบัติ) = สนามแม่เหล็กโลกที่ BMM350 วัด · Brosen (CC BY 2.5) = ช่องทิศ 8 ช่องที่ `DIRS` แบ่ง · ล่าง: NOAA NCEI / BGS, World Magnetic Model 2025.0 (สาธารณสมบัติ) — ค่าเบี่ยงเบนแม่เหล็กทั่วโลก ตัวเลของศาที่การ์ดแสดงยังไม่ใช่ทิศเหนือจริง ต้องบวกค่าจากแผนที่นี้ก่อน

ตัวเลขให้ความแม่นยำ ตัวอักษรทิศให้ความหมาย คนที่ยืนดูใช้อย่างหลังก่อนเสมอ

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 ยืนยันว่ากำลังคุยกับชิปถูกตัว ตอนสงสัยว่าสายหลุด

สามข้อที่ทำให้เข็มทิศบนการ์ดนิ่งหรือไม่นิ่ง

  1. มีแต่ heading() เท่านั้นที่ขยับตัวคาลิเบรต — magnetic() ไม่ได้ป้อนอะไรให้มันเลย โปรแกรมที่เรียกแต่ magnetic() จะรอค่าชดเชยจนวันตายก็ไม่ลู่เข้า
  2. heading() คืนค่าเฉลี่ยของสิบครั้งหลังสุด เฉลี่ยแบบวงกลมเพื่อไม่ให้พังตอนข้ามรอยต่อ 0 กับ 360 องศา สิบครั้งแรกหลังบูตจึงเป็นค่าที่ยังเฉลี่ยไม่เต็มถัง — อย่าเพิ่งตัดสินจากค่าแรก
  3. 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 นั่นคือข้อมูลใหม่ที่ยังไม่มีใครมี ให้บอกผู้สอน

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

แกะโค้ดจริง — ท่าที่ 4 การ์ดสัมผัสและการ์ดลูกบิด

ui.Slider — ของที่ "คนลาก" บนจอ นิ้วคน ใช้ผิดที่ = คนดูเข้าใจว่าลากบนจอได้ ui.Bar — ของที่ "เครื่องแสดง" CapSense ค่ามาจากแถบสัมผัสจริงบนบอร์ด ไม่ใช่จากจอ การ์ดลูกบิด — Arc กับ Seg7 ตอบคนละคำถาม Arc = "อยู่ตรงไหนของช่วง" รู้ในพริบตา 48.2 Seg7 = ตัวเลขที่ จดลงใบงานได้
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']))

ภาพ: Arctanx / Wikimedia Commons — สาธารณสมบัติ · สัญญาณเด้งของหน้าสัมผัสจริงราว 250 ไมโครวินาที — ตัวเลขที่ตอบว่าการ์ดสัมผัสต้องรอนานแค่ไหนก่อนเชื่อค่า

เลือก widget ตาม "ใครเป็นคนเปลี่ยนค่านี้" ไม่ใช่ตามว่าอันไหนดูดีกว่า

แกะโค้ดจริง — ท่าที่ 5 ลูปหลัก และการอ่านเซนเซอร์แบบ sync

t0 = ticks_ms() จับเวลาต้นรอบ อ่าน 4 ตัว sync แตะบัสให้น้อยครั้ง อัปเดต widget set_next · text · value ui.poll() ระบายคิวฝั่ง CM55 sleep_ms(200) คืนเวลาให้ระบบ ลืม ui.poll() แล้วจอจะซ่อน widget ประมาณสองวินาที แล้วกลับมาใหม่ วนแบบนั้น รันสิบนาทีที่ 200 ms = ประมาณสามพันรอบ บรรทัดที่แพงจะโผล่ให้เห็นเอง
    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

ทุกบรรทัดในลูปนี้จะถูกรันประมาณสามพันรอบระหว่างการทดสอบสิบนาที บรรทัดที่แพงจะโผล่ให้เห็นเอง

ข้อมูลไหลไปทางไหน — สี่เซนเซอร์ หนึ่งลูป หนึ่งหน้าจอ

BMI270 BMM350 CapSense Potentiometer ลูป Python บน CM33 อ่าน sync ทั้งสี่ตัว แปลงหน่วย + จัดรูปแบบ sleep_ms(200) IPC คิวคำสั่งวาด ตาราง 64 ช่อง CM55 + LVGL วาดการ์ดสี่ใบ 23 widgets จอ 792 x 398 คอขวดอยู่ที่นี่: ถ้าลูปเร็วเกิน คิวฝั่งขวาจะล้นแบบเงียบ ๆ

ดูเพิ่ม (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 แบบสองไบต์ต่อตัวอย่าง) และมันจ่าย ตัวที่เก่าที่สุดก่อน

Tring=19,999 ไบต์2 ไบต์/ตัวอย่าง×16,000 ตัวอย่าง/วินาที=0.625 วินาทีT_{\text{ring}}=\frac{19{,}999\ \text{ไบต์}}{2\ \text{ไบต์/ตัวอย่าง}\times 16{,}000\ \text{ตัวอย่าง/วินาที}} = 0.625\ \text{วินาที}

ผลคือ ลูปที่อ่านช้ากว่าที่ไมค์ผลิต จะตามหลังถาวร และเพดานของการตามหลังคือ 625 ms — สิ่งที่การ์ดแสดงจะเป็นห้องเมื่อครึ่งวินาทีที่แล้ว ไม่ใช่ห้องตอนนี้ ทางแก้มีอยู่แล้วในตัว: fresh=True ซึ่งเป็น ค่าตั้งต้น ของ stats() แปลว่า "ทิ้งของค้างทั้งหมด แล้วเอาเฉพาะหน้าต่างล่าสุด"

อ่านแบบไหน lag() ที่วัดได้จริงบนบอร์ด 14 ส.ค. 2026
stats(fresh=True) (ค่าตั้งต้น) 0 ถึง 48 ms
stats(fresh=False) — เก็บของค้างไว้ 496 ถึง 624 ms

read() ใช้ทางที่ ไม่ทิ้ง เสมอ อยากได้รูปคลื่นสด ๆ ต้องอ่านให้ทันเอง หรือเรียก stats() นำหน้าเพื่อเคลียร์คิวก่อน

level() เป็นสเกลอ็อกเทฟ ไม่ใช่เปอร์เซ็นต์ของสเกลเต็ม

level=(log⁡2(rms)×256−1024)×1002816,rms<16⇒0\text{level} = \frac{\bigl(\log_2(\text{rms})\times 256 - 1024\bigr)\times 100}{2816},\qquad \text{rms} < 16 \Rightarrow 0

ไทย: ทุกครั้งที่ 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 ให้เห็นคิวโตกับตา)

ตัวเลขความดังที่อ่านมาช้ากว่าความจริงครึ่งวินาที ยังเป็นตัวเลขที่ถูก — แต่มันตอบคนละคำถามกับที่คนดูจอกำลังถาม

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

1 · เปิด Playground ค้างไว้ ห้ามกดกลับระหว่างทดสอบ 2 · เปิดไฟล์ฝึกใน IDE s08_dashboard.py 3 · วาดผังบนกระดาษ กรอกตารางงบ widget ให้ครบ 4 · เติมทีละจุด แล้วรัน เจ็ดจุด เจ็ดครั้ง ห้ามรวบ 5 · แก้ชื่อทีมในบรรทัด head ให้เป็นของทีมเราเอง 6 · รันทิ้งไว้ 10 นาที จดเลขรอบทุกสองนาที
  1. บนจอบอร์ด แตะการ์ด BENTO Playground แล้วค้างหน้านี้ไว้ ห้ามกดกลับระหว่างทดสอบ
  2. เปิด practise_codes/s08_dashboard.py↗ ใน BENTO IDE
  3. วาดผังบนกระดาษก่อน — กรอกตารางงบ widget ในใบงานข้อ 4 ให้ครบก่อนแตะคีย์บอร์ด
  4. เติมช่องว่างทั้ง 7 จุด ทีละจุด แล้วกด Program to Device ดูผลทุกครั้ง
  5. เมื่อครบทั้งเจ็ดจุดแล้ว แก้ชื่อทีมในบรรทัด head ให้เป็นของทีมเรา
  6. รันทิ้งไว้ 10 นาที พร้อมจับเวลาและจดเลขรอบตามใบงาน

ถ้าจอค้างระหว่างทาง กด RESTART บนหน้า Playground แล้วเริ่มจับเวลาใหม่ตั้งแต่ศูนย์ — ห้ามนับต่อ

ห้ามเติมครบเจ็ดจุดแล้วค่อยรันทีเดียว ถ้าพังจะไม่รู้เลยว่าพังที่จุดไหนในเจ็ดจุด

วิธีทดสอบ soak run 10 นาที — และแยก "ค้าง" ออกจาก "ช้า"

ปกติ — loop ms คงที่ รอบ 590-600 ต่อสองนาที · loop 3 ms ตลอด ช้าลง — ไม่ใช่ค้าง เลขรอบยังเดิน แต่ loop โตจาก 3 เป็น 40 ms วิธีแยกที่เร็วที่สุด: หมุนลูกบิด แล้วดูสามที่พร้อมกัน — Seg7 · เลขรอบ · loop ms

การรันสิบนาทีไม่ใช่การนั่งรอเฉย ๆ เรากำลังเก็บข้อมูลอยู่ จดเลขรอบทุกสองนาที

ที่ cadence 200 ms ลูปควรเดินราว 5 รอบต่อวินาที = ประมาณ 600 รอบต่อสองนาที ถ้าจดได้ 590–600 ทุกช่วง แปลว่าคงที่ ถ้าช่วงหลังเหลือ 300 แปลว่าเริ่มช้าลงแล้ว

อาการที่เห็น เลขรอบ loop ms แปลว่า
ทุกอย่างนิ่งสนิท ภาพค้างที่เฟรมสุดท้าย หยุด หยุด ค้างจริง — ลูปตายหรือ CM55 หยุดตอบ
ตัวเลขยังเดินแต่ช้าลงเรื่อย ๆ เดินต่อ โตขึ้น 3 → 40 ช้า ไม่ใช่ค้าง — มีอะไรสะสมในลูป
จอนิ่ง แต่คอนโซลขึ้น Traceback หยุด หยุด สคริปต์ตายที่ Python — อ่าน error ได้ตรง ๆ
widget หายไปแวบหนึ่งแล้วกลับมา เดินต่อ ปกติ ลืม ui.poll() ในลูป
ค่าเซนเซอร์ค้างค่าเดิม แต่รอบยังเดิน เดินต่อ ปกติ เซนเซอร์ไม่ตอบ ไม่ใช่จอมีปัญหา

วิธีแยกที่เร็วที่สุด: หมุนลูกบิด แล้วดูสามที่พร้อมกัน — เลข Seg7, เลขรอบ, และ loop ms ถ้าเลขรอบเดินแต่ Seg7 ไม่ขยับ ปัญหาอยู่ฝั่งเซนเซอร์ ถ้าเลขรอบหยุด ปัญหาอยู่ฝั่งลูปหรือจอ

อีกอย่างที่ต้องดูคือ ความร้อนและการเสียบสาย — สาย USB ที่หลวมทำให้บอร์ดรีเซ็ตกลางทาง แล้วเราจะไปโทษโค้ดผิด ๆ

"ค้าง" กับ "ช้า" แก้คนละวิธีกันสิ้นเชิง เสียเวลาห้าวินาทีแยกให้ออกก่อน ประหยัดไปได้ครึ่งชั่วโมง

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

dashboard 4 การ์ดรันต่อเนื่อง 10 นาทีไม่ค้างไม่ crash

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

  • [ ] มีการ์ดครบสี่ใบบนจอเดียว ไม่ทับกัน ไม่ล้นขอบ 792×398
  • [ ] การ์ด IMU มีกราฟที่วิ่งตามการเขย่าบอร์ดจริง
  • [ ] เข็มทิศหมุนตามการหันบอร์ด และตัวอักษรทิศเปลี่ยนตามองศา
  • [ ] แตะปุ่ม CapSense แล้วไฟ ui.Led ดวงนั้นติด (ปล่อยแล้วหรี่ ไม่ใช่หาย) · เลื่อนนิ้วแล้ว Bar กับตัวเลข % ขยับ
  • [ ] หมุนลูกบิดแล้ว Arc และ Seg7 เปลี่ยนพร้อมกัน
  • [ ] งบ widget ที่นับได้ ไม่เกิน 32 ที่ตั้งไว้ (เพดานเฟิร์มแวร์ 64) และตัวเลขในหัวไฟล์ตรงกับของจริง
  • [ ] รันต่อเนื่อง 10 นาที เลขรอบเดินตลอด ไม่มี Traceback และ loop ms ไม่โตขึ้นเรื่อย ๆ
  • [ ] จดเลขรอบทุกสองนาทีลงใบงานครบหกช่อง

ห้าข้อแรกใช้เวลาสร้างหนึ่งชั่วโมง ข้อที่เจ็ดใช้เวลาสิบนาทีที่ห้ามลัด — และมันคือข้อที่แยกของเล่นออกจากของใช้งานจริง

กับดักที่เจอบ่อย

อาการ สาเหตุที่แท้จริง วิธีแก้
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

ลงมือทำ — เติมช่องว่างในไฟล์ฝึก

เติมแล้วรัน — ควรเห็นการ์ดว่าง เติมทีละจุด แล้วรันทุกครั้ง — ค่าจะมาทีละการ์ด จุดสุดท้าย จุด 1 อ่านทิ้ง 1 ครั้ง จุด 2 Panel จุด 3 chart จุด 4 compass จุด 5 bar จุด 6 seg7 จุด 7 ui.poll() จงใจลองผิดหนึ่งรอบ: รันครึ่งนาทีก่อนเติมจุดที่ 7 แล้วจดว่าจอมีอาการอะไร นั่นคืออาการของการลืม ui.poll() ซึ่งจะเจอได้อีกในโปรเจกต์จริง

เปิด 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()

เติมทีละจุดแล้วรัน คือวิธีเดียวที่ทำให้รู้ว่าจุดไหนพัง เมื่อโค้ดยาวเกินสามสิบบรรทัด

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

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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() ครบทั้งสามชื่อที่เหลือ

เชื่อมโยงรากฐาน — วันนี้เราแตะอะไรไปบ้าง

ระบบสมองกลฝังตัว จองทรัพยากรแบบคงที่ ทำงานภายใต้งบที่นับได้ soak run ทดสอบความทน อ่านอุปกรณ์แบบ sync ในลูปเดียว Python และ CS try/except กันลูปตาย เก็บ handle ไว้สั่งทีหลัง ticks_diff กันเวลาล้น แปลงค่าต่อเนื่องเป็นหมวด องศา 137 ไปเป็น SE การออกแบบระบบ จัดกลุ่มตามคำถามที่มันตอบ ลำดับความสำคัญทางสายตา สีเป็นภาษา ไม่ใช่การตกแต่ง เลือก widget ตามผู้เปลี่ยนค่า ออกแบบใต้ข้อจำกัดที่แก้ไม่ได้

ฝั่งระบบสมองกลฝังตัว
การจองทรัพยากรแบบคงที่ (ตาราง handle 64 ช่อง) และเหตุผลว่าทำไมระบบที่ต้องทำงานยาวถึงเลือกทางนี้ · การทำงานภายใต้งบที่นับได้ · การทดสอบความทนทานด้วย soak run · การอ่านอุปกรณ์แบบ synchronous ในลูปเดียว

ฝั่ง Python และวิทยาการคอมพิวเตอร์
try/except กันลูปตายเพราะเซนเซอร์ตัวเดียว · การเก็บ handle ของอ็อบเจกต์ไว้ในตัวแปรเพื่อสั่งงานทีหลัง · time.ticks_ms() / ticks_diff() กับการวัดเวลาแบบไม่ล้น · การแปลงค่าต่อเนื่องเป็นหมวด (องศา → ชื่อทิศ)

ฝั่งการออกแบบระบบ
การจัดกลุ่มข้อมูลตามคำถามที่มันตอบ · ลำดับความสำคัญทางสายตา · การใช้สีเป็นภาษาแทนการตกแต่ง · การเลือก widget ตามว่าใครเป็นคนเปลี่ยนค่า · การออกแบบภายใต้ข้อจำกัดที่แก้ไม่ได้

ข้อสุดท้ายคือของที่ใช้ได้แม้เปลี่ยนภาษา เปลี่ยนบอร์ด และเปลี่ยนอาชีพ

งานทำเอง 30% + สรุปคาบ

วันนี้ · คาบ 8 IMU Compass CapSense Pot ต่อยอด คาบหน้า · คาบ 9 บนหน้าจอใบเดิม IMU Compass CapSense SSID · IP ping

วันนี้เราได้:
ออกแบบผัง 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 # หัวเรื่อง + ไฟค่าค้าง + สถานะ = 4 # การ์ด IMU = 4 # การ์ดเข็มทิศ = 5 # การ์ดสัมผัส (ไฟ 2 + Scale) = 9 # การ์ด pot = 5 # แถบคำสั่ง (ปุ่ม Spinbox ไฟ) = 5 เอกสารอยู่บรรทัดบนโค้ด ใช้ไป 32 64 เอกสารที่อยู่ไกลจากโค้ดจะเก่าเสมอ เอกสารที่อยู่บรรทัดบนโค้ด มีโอกาสถูกแก้ตาม

อ่านให้เข้าใจ แล้วพิมพ์เอง อย่าคัดลอกวาง

# งบ 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 เพราะเราไม่ได้คำนวณอะไรที่ต้องใช้

เอกสารที่อยู่ไกลจากโค้ดจะเก่าเสมอ เอกสารที่อยู่บรรทัดบนโค้ดมีโอกาสถูกแก้ตาม

เฉลย — ส่วนที่สอง: การ์ดสี่ใบ

แนวนอน (แถวบน) — บวกให้ครบ 792 การ์ดซ้าย w=368 การ์ดขวา w=360 24 16 24 24 + 368 + 16 + 360 + 24 = 792 แถวล่าง: 24 + 320 + 16 + 408 + 24 = 792 แนวตั้ง — บวกให้ครบ 398 แถบหัว 92 แถวบน 136 16 แถวล่าง 136 92 + 8 + 136 + 16 + 136 + 10 = 398
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 ครอบทีละตัว — ที่เฉลยใช้ BMM350 ไม่ตอบ คงค่าเดิม จุดไฟค่าค้าง BMI270 ปกติ กราฟยังวิ่ง CapSense ปกติ Bar ยังขยับ Pot ปกติ Seg7 ยังเปลี่ยน try ครอบทั้งลูป — สิ่งที่ห้ามทำ BMM350 ไม่ตอบ โดดออกจากลูป ไม่ได้อัปเดต ไม่ได้อัปเดต ไม่ได้อัปเดต การเลือกขอบเขตของ try คือการตัดสินใจว่า "อะไรพังได้ โดยที่ระบบยังใช้งานได้อยู่"
try/except ครอบการอ่านเซนเซอร์ ทีละตัว ไม่ใช่ครอบทั้งลูป — เข็มทิศตัวเดียวมีปัญหา อีกสามการ์ดยังต้องทำงานต่อ · ticks_diff() แทนการลบตรง ๆ เพราะ ticks_ms() วนกลับเป็นศูนย์ในวันที่รันยาว · except ไม่เขียนศูนย์ทับค่าเดิม — ศูนย์คือตัวเลขที่หน้าตาเหมือนค่าที่วัดมาจริง รุ่นนี้คงค่าล่าสุดไว้แล้วจุดไฟ "ค่าค้าง" ตอบคำถามที่แดชบอร์ดทุกใบต้องตอบ: เลขที่เห็นอยู่ตอนนี้ ใช่ค่าปัจจุบันไหม · ตัวเลขขยับไม่เกินวินาทีละครั้ง ทั้งที่ลูปเดินทุก 200 ms — กราฟ แถบ เข็ม และไฟขยับที่ 200 ms ได้ เพราะตาอ่านรูปทรง แต่ตัวเลขที่กระพริบห้าครั้งต่อวินาทีไม่มีใครอ่านทัน

การเลือกขอบเขตของ try คือการตัดสินใจว่า "อะไรพังได้โดยที่ระบบยังใช้งานได้อยู่"

เฉลย · ทำไมเรียงหกท่าแบบนี้ ไม่ใช่สุ่มเรียง

ท่า 1 · ราก จอ สี เซนเซอร์ ท่า 2 · ใบที่ยากสุด Panel + Chart + series ท่า 3 · ทำซ้ำ การ์ดเข็มทิศ ท่า 4 · ทำซ้ำ สัมผัส + ลูกบิด ท่า 5-6 · คำสั่ง + ลูป ทุกอย่างมาเจอกัน สร้างของนิ่งให้ครบก่อน แล้วค่อยใส่ของที่เคลื่อนไหว ของนิ่งพังจะเห็นทันทีจากตา ของเคลื่อนไหวพังต้องนั่งดูสักพัก

ท่า 1 เตรียมจอ ชุดสี เซนเซอร์ มาก่อน เพราะทั้งสามอย่างคือรากที่ท่าอื่นยืนอยู่บน ถ้า ui.screen() ไม่ทำงาน หรือเซนเซอร์ยังไม่ตื่น ที่เหลือไม่มีประโยชน์จะเขียนต่อ

ท่า 2 การ์ด IMU มาเป็นใบแรกเพราะมันซับซ้อนที่สุด (Panel + Chart + series สามเส้น) ถ้าใบยากที่สุดผ่านแล้ว ใบที่เหลือคือการทำซ้ำแบบเดียวกัน — ทำของยากตอนที่ยังมีแรงและยังมีเวลาเหลือ

ท่า 3–4 การ์ดที่เหลือ เรียงตามผังจากซ้ายไปขวา บนลงล่าง ตรงกับที่ตาคนอ่าน ทำให้เวลากลับมาแก้ หาตำแหน่งในไฟล์ได้จากตำแหน่งบนจอ

ท่า 5 แถบคำสั่ง มาก่อนลูป เพราะปุ่มกับ Spinbox ต้องมีตัวตนอยู่ก่อน ลูปถึงจะอ้าง btn_run.id() ได้ และเพราะมันคือส่วนที่ทำให้แดชบอร์ดใบนี้ เป็นแผงควบคุม ไม่ใช่โปสเตอร์ที่ตัวเลขขยับได้ แดชบอร์ดที่ผู้ใช้แตะอะไรไม่ได้เลย ตอบได้แค่ "ตอนนี้เป็นยังไง" แต่ตอบไม่ได้ว่า "แล้วจะให้ทำอะไรต่อ"

ท่า 6 ลูปหลัก มาสุดท้ายเสมอ เพราะลูปคือที่ที่ทุกอย่างมาเจอกัน ถ้าเขียนลูปก่อนสร้าง widget โปรแกรมจะพังที่ชื่อตัวแปรที่ยังไม่มี

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

ลำดับที่ดีคือลำดับที่ทำให้ "รู้ว่าพังตรงไหน" ได้เร็วที่สุด ไม่ใช่ลำดับที่พิมพ์แล้วลื่นที่สุด

เชื่อมจุดให้เห็นภาพ — วันนี้อยู่ตรงไหนของเส้นทาง

คาบ 3-5 คุมฮาร์ดแวร์ทีละชิ้น ปุ่ม ไฟ ลูกบิด สัมผัส คาบ 6-7 แปลงข้อมูลดิบเป็นความหมาย มุมเอียง · กราฟเวลาจริง วันนี้ · คาบ 8 รวมทุกชิ้นเป็น HMI เดียว ภายใต้งบและ cadence จริง คาบ 9-12 ต่อเน็ต ส่งขึ้นแพลตฟอร์ม บนหน้าจอใบเดิมนี้ คาบ 8 คือจุดที่ของทุกชิ้นมาบรรจบ แล้วกลายเป็นฐานของอีกสี่คาบข้างหน้า

คำถามคิดต่อ: ถ้าต้องเพิ่มการ์ดสถานะเครือข่ายในคาบหน้า จะตัดอะไรออกหรือใช้งบที่เหลือ · การ์ดใบไหนที่คนดูจริงจะมองบ่อยที่สุด และมันควรอยู่ตำแหน่งนั้นไหม

ใช้จริงที่ไหน — HMI สี่แบบในสนามจริง

โรงงาน · แผงข้างเครื่องจักร การ์ดความสั่น · อุณหภูมิ · รอบมอเตอร์ · สถานะ ต้องอ่านออกจากระยะ 3 เมตร ขณะใส่ถุงมือ ตรงกับวันนี้: ลำดับสายตา + สีสามระดับ อาคาร · ห้องควบคุมระบบ การ์ดต่อชั้น: แอร์ ไฟ คุณภาพอากาศ คนในพื้นที่ เปิดค้างไว้ 24 ชั่วโมง ห้ามค้างห้ามหน่วง ตรงกับวันนี้: soak test 10 นาทีคือฉบับย่อ ยานพาหนะ · แผงหน้าปัด เข็มทิศ ความเร็ว มุมเอียง อยู่บนจอเดียว คนขับมองได้ครั้งละไม่เกินครึ่งวินาที ตรงกับวันนี้: เข็มทิศ + ตัวเลขใหญ่ 28 px เกษตร · ตู้ควบคุมโรงเรือน การ์ดความชื้น น้ำ พัดลม พร้อมปุ่มสัมผัส ทำงานคนเดียวกลางแดด ไม่มีคีย์บอร์ด ตรงกับวันนี้: Bar อ่านอย่างเดียว vs Slider ที่ลากได้

ภาพ: Axel Hindemith / Wikimedia Commons — CC BY-SA 3.0 · การสำรวจสนามแม่เหล็กในภาคสนามจริง — งานที่ใช้ค่าจากเข็มทิศเป็นข้อมูลหลัก ไม่ใช่ของประดับหน้าจอ

เซนเซอร์ที่การ์ดสองใบล่างต้องใช้ — 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 ข้อ)

ข้อ 1 · การ์ดใบที่ห้า: สถานะระบบ เวลารันสะสม · จำนวนรอบ · loop ms สูงสุด ต้องบอกด้วยว่าใช้งบที่เหลือหรือตัดอะไรออก ข้อ 2 · โซนสีตามเกณฑ์ เขียว ต่ำกว่า 11 · เหลือง 11-12 · แดง เกิน 12 เขียนด้วยว่าทำไมถึงเลือกเกณฑ์นี้ ข้อ 3 · สลับหน้าด้วย show() / hide() ภาพรวม กับ IMU เต็มจอ — ซ่อนแทนการลบ ประหยัดงบ widget อย่างไรเมื่อเทียบกับสร้างใหม่ ข้อ 4 · รายงานผล soak run 10 นาทีสองรอบ ที่ 200 ms และที่ 80 ms สรุปครึ่งหน้าว่าทีมจะเลือก cadence เท่าไร

ข้อ 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 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า

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

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

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

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

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

เอกสารบอร์ดและเซนเซอร์ (แหล่งปฐมภูมิ)

  • KIT_PSE84_EVAL Kit guide, Infineon 002-39007 Rev.*B — §3.2.2.13 (หน้า 87) · §3.2.2.14 (หน้า 88)
  • BMM350 datasheet, BST-BMM350-DS001-27 rev 1.27 — Bosch Sensortec — bst-bmm350-ds001.pdf บน bosch-sensortec.com
  • DPS368 datasheet — Infineon (เซนเซอร์ที่ ไม่มี บน Eva Kit · มี บน TESAIoT Dev Kit)
  • Practical Electronics for Inventors, 4th ed. — §2.31 Decibels · §6.2.1 Thermistors · §6.4.8 Pressure

ซอฟต์แวร์

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

  • RLQGZl0lpjQ Working principle of an accelerometer — Bosch Sensortec
  • 6BS6aQBaMhU Projected Capacitive Touch Technology - How It Works — Zytronic Displays
  • 0bw5UJQxkhA Barometric pressure sensor: working principle — Bosch Sensortec
  • J2ulD-bPTS4 Understanding Temperature Sensor Technology — RealPars

เพดาน 64 widgets · ความหมาย kwarg ของ ui.Panel · ขนาดจอ 792x398 — มาจากซอร์สเฟิร์มแวร์เอง ดู path ในสไลด์ผู้สอน

อ้างอิงและเครดิต (ต่อ) — ภาพประกอบ

ภาพประกอบ

  • สนามแม่เหล็กโลกแบบไดโพล: NASA/USGS / Wikimedia Commons — สาธารณสมบัติ
  • กุหลาบทิศ (wind rose): Brosen / Wikimedia Commons — CC BY 2.5
  • แผนที่ค่าเบี่ยงเบนแม่เหล็กโลก 2025: NOAA NCEI / BGS, World Magnetic Model 2025.0 — สาธารณสมบัติ
  • ปรากฏการณ์ฮอลล์สี่กรณี: Peo / Hike395 / Wikimedia Commons — CC BY-SA 3.0
  • แกนลำตัวกับการติดตั้ง accelerometer: Rodriguez V.H. et al., Sensors 22(20):7690 (2022) — CC BY 4.0
  • แรงโน้มถ่วงในลิฟต์: Vkidambi / Wikimedia Commons — CC BY-SA 4.0
  • สัญญาณสั่นสะเทือนดิบจากตลับลูกปืน: Sehri M. et al., Data in Brief (2023) — CC BY 4.0
  • ความเร่งจากการเดิน แขนเทียบข้อเท้า: Kisiel M. et al., Sensors 26(3):876 (2026) — CC BY 4.0
  • หน้าต่างเฉลี่ยแบบเลื่อน: b_sliding_window_smoothing_animation.gif / Wikimedia Commons — CC0 1.0
  • สัญญาณเด้งของหน้าสัมผัส 250 ไมโครวินาที: Arctanx / Wikimedia Commons — สาธารณสมบัติ
  • เสาไฟสถานะเครื่องจักร: User:Mattes / Wikimedia Commons — สาธารณสมบัติ
  • แผงเฟดเดอร์ของมิกเซอร์: hanmaili / Wikimedia Commons — CC0 1.0
  • การสำรวจสนามแม่เหล็กภาคสนาม: Axel Hindemith / Wikimedia Commons — CC BY-SA 3.0
  • ไดอะแกรมอื่นทุกภาพวาดขึ้นใหม่สำหรับหลักสูตรนี้ ไม่ได้คัดลอกจากเอกสารผู้ผลิต

การ์ดที่มีชื่อกำกับมาในตัว

ภาพจากตัวจำลอง Eva Kit (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 เปิดตอนงบเริ่มตึงแล้วอยากรู้ว่าเหลือเท่าไรจริง ๆ

☰ สารบัญ