| คำถาม | คำตอบของคาบนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| Why | ตัวเลขบนจอก็อ่านค่าได้อยู่แล้ว ทำไมต้องมีกราฟ | เพราะตัวเลขตอบได้คำถามเดียวคือ "ตอนนี้เท่าไร" ส่วนคำถามที่งานจริงถามคือ "เมื่อกี้มันเป็นยังไง" และ "มันกำลังจะไปทางไหน" · และการสุ่มไม่ทันไม่ส่งเสียงเตือน มันให้คำตอบที่ผิดและดูน่าเชื่อถือ ต้องรู้ล่วงหน้าว่าสัญญาณของเราเร็วแค่ไหน | ครึ่งแรก · Nyquist และ aliasing |
| What | มีอะไรให้ใช้บ้าง | ui.Chart กับเมธอดของ Widget ทั้ง 14 ตัว — สองตัวเป็นของกราฟล้วน ๆ · บวกตัวสร้าง widget ของ ui ทั้ง 16 ตัว ที่กางครบในตารางอ้างอิงของคาบนี้ |
สไลด์บัญชี 14 เมธอด + ตารางอ้างอิง widget |
| How | ประกอบยังไงให้ใช้งานได้จริง | สร้างกราฟหนึ่งใบสามเส้น ป้อนด้วย int() ทุกรอบ · ปุ่มเริ่ม/หยุดบันทึกที่เป็น flag ในลูป ไม่ใช่การหยุดลูป · ตารางเก็บค่าสุดขีดที่กราฟจำไม่ได้ · และนาฬิกาที่จับคาบลูปจริงมาโชว์ |
สามไฟล์ตัวอย่าง + ใบฝึก |
ปลายทางที่จับต้องได้ — กราฟสามสีของแกน X Y Z วิ่งตามการเขย่ามือ มีปุ่มหยุด-เดินต่อที่ทำงานจริง และบรรทัดที่บอกว่าลูปของเราหมุนอยู่ที่กี่มิลลิวินาที
คาบ 6 เราวัด "ตอนนี้เอียงกี่องศา" · คาบนี้เราเก็บ "สิบวินาทีที่ผ่านมาเป็นยังไง"
ui.Chart กับ .add_series() และ .set_next() ได้ถูกต้อง รวมถึงรู้ว่าทำไมต้อง int()time.ticks_ms() / time.ticks_diff() แล้วเอาขึ้นจอset_next() ถึงวาดช้ากว่าลูปที่มี .text() อยู่ด้วยถึง 40 เท่าปลายทางของวันนี้: จอบอร์ดมีกราฟสามสีวิ่งตามการเขย่า ตารางค่าสุดขีดสามแกน ปุ่มเริ่ม/หยุดบันทึก และตัวเลขบอกว่าลูปของเราหมุนจริงที่กี่มิลลิวินาที
คาบนี้เราไม่ได้แค่ทำให้กราฟขึ้น เราต้องรู้ด้วยว่ากราฟนั้น เชื่อถือได้แค่ไหน

หน้าจอของเฉลยปัจจุบันแบ่งเป็นสามส่วน และสองส่วนแรกตอบคนละคำถามกัน
ui.Chart เส้นสามสีของแกน X Y Z ตอบคำถามว่า "เมื่อกี้มันเป็นยังไง" (ตอนบอร์ดวางนิ่ง เส้นจึงราบ)ui.Table สี่คอลัมน์ แกน · ต่ำสุด · สูงสุด · ล่าสุด ตอบคำถามที่กราฟตอบไม่ได้ คือ ค่าสุดขีดที่ผ่านไปแล้ว เพราะกราฟเก็บได้เท่าจำนวนช่องของมัน (ปริยาย 50) จุดที่เก่ากว่านั้นถูกเขี่ยทิ้งไปแล้วui.Led บอกว่ากำลังบันทึกอยู่หรือหยุดแล้ว บรรทัดคาบลูปจริง และ ปุ่มเริ่มกับปุ่มหยุดที่แยกกันคนละปุ่ม (สูง 88 px ตามขนาดเป้าสัมผัส)กราฟตอบ "เมื่อกี้มันเป็นยังไง" ส่วนตารางตอบ "ที่ผ่านมาแรงสุดเท่าไร" — คนละคำถาม จึงต้องมีทั้งคู่
คาบ 6 เราอ่าน sensors.bmi270.motion() → dsp.tilt(ax, ay, az) → (roll, pitch) → ui.Bar บน ui.Scale สองแกน + ui.Seg7
สิ่งที่แถบกับ Seg7 ทำได้ดี: บอก ค่าปัจจุบัน ชัดเจน · สิ่งที่ทำไม่ได้เลย: บอกว่า เมื่อกี้เป็นยังไง
| คำถามที่ทีมอยากรู้ | Bar / Seg7 | Chart |
|---|---|---|
| ตอนนี้เอียงกี่องศา | ตอบได้ทันที | ต้องเพ่งที่ปลายเส้น |
| เมื่อ 5 วินาทีที่แล้วสั่นไหม | ตอบไม่ได้ | ตอบได้ |
| การสั่นถี่ขึ้นหรือเบาลง | ตอบไม่ได้ | เห็นเป็นรูปร่างทันที |
| มีค่ากระโดดผิดปกติแวบเดียวไหม | พลาดแน่นอน | เห็นเป็นหนามบนเส้น |
ภาพขวาคือสัญญาณ accel จริงตอนคนเดิน 10 ก้าว — ยอดที่ทำเครื่องหมายสีแดงคือก้าว ข้อมูลชุดนี้อ่านจากแถบหรือตัวเลขไม่ได้เลย ต้องเห็นเป็นเส้นเวลาเท่านั้น

เลือก widget ตาม คำถามที่ต้องการคำตอบ ไม่ใช่ตามความสวย
Time series คือข้อมูลชุดหนึ่งที่ แต่ละค่ามีเวลากำกับ และเรียงตามเวลา
เซนเซอร์ให้สัญญาณต่อเนื่องซึ่งมีค่าอยู่ทุกเสี้ยววินาที แต่คอมพิวเตอร์เก็บค่าต่อเนื่องไม่ได้ มันทำได้อย่างเดียวคือ แอบมองเป็นระยะ ๆ แล้วจดค่าไว้ — เรียกว่า การสุ่มตัวอย่าง (sampling)
ไทย: อัตราสุ่มคือส่วนกลับของระยะห่างระหว่างการมองสองครั้ง
ตัวเลขของลูปเรา: ms Hz
วงจร sample-and-hold (ภาพขวาบน) คือฮาร์ดแวร์ที่ทำเรื่องนี้จริง ๆ ในชิป: ปิดสวิตช์ชั่วขณะเพื่อชาร์จตัวเก็บประจุ แล้วเปิดสวิตช์ค้างค่านั้นไว้ให้ ADC อ่านทัน — เอาต์พุตจึงเป็นขั้นบันได ไม่ใช่เส้นโค้ง
กราฟบนจอไม่ใช่สัญญาณจริง มันคือ จุดที่เราจดไว้ แล้วลากเส้นตรงเชื่อมกัน ระหว่างจุดสองจุดเกิดอะไรขึ้นบ้าง เราไม่มีทางรู้จากกราฟนี้เลย
กราฟทุกกราฟในโลก embedded คือ "ค่าที่เราเลือกจะมอง" ไม่ใช่ "ทุกอย่างที่เกิดขึ้น"

ปี 1928 แฮร์รี ไนควิสต์ พิสูจน์ไว้ว่า ถ้าจะเก็บสัญญาณที่มีความถี่สูงสุด ให้ครบ ต้องสุ่มเร็วกว่าสองเท่าของมัน
ไทย: จะเห็นคลื่นความถี่หนึ่งได้ ต้องสุ่มเร็วกว่าสองเท่าของคลื่นนั้น
ตัวเลขของลูปเรา: Hz เห็นการสั่นได้ไม่เกิน 2.5 Hz
การเขย่ามือของคนอยู่ราว 2-5 Hz เราจึงเห็นมันได้ · อัตราสุ่มของเราไม่ได้ตั้งด้วยฮาร์ดแวร์ แต่เกิดจากความเร็วลูป Python
ไทย: งานในลูปหนักขึ้น อัตราสุ่มตกลงเงียบ ๆ — จึงต้องวัดคาบลูปจริง (ท่าที่ 5)
สุ่มไม่ทันไม่ได้แปลว่า "มองไม่เห็น" มันแปลว่า "เห็นผิด" — สไลด์ถัดไปคือเหตุผล
ไทย: ถ้าสัญญาณเร็วเกินไป มันจะไม่หายไปเฉย ๆ แต่จะ ปลอมตัว มาเป็นคลื่นช้ากว่าความจริง ซึ่งอันตรายกว่าการมองไม่เห็น
ตัวเลขจากบอร์ดนี้ — เดโมที่ทำได้ในคาบ: เขย่าบอร์ดที่ราว 6 Hz ขณะลูปสุ่มที่ Hz
กราฟจะโชว์คลื่นช้า ๆ 1 Hz ที่ ไม่มีอยู่จริง ทั้งที่มือเราเขย่าอยู่ 6 ครั้งต่อวินาที
นี่คือเหตุผลเดียวกับที่ล้อรถในหนังบางทีดูเหมือนหมุนถอยหลัง — กล้องคือ ADC ที่สุ่มที่ 24 เฟรมต่อวินาที
เจอตอนไหน ค่าที่เร็วเกินอัตราสุ่มไม่มี error ไม่มีคำเตือน มันแค่ให้คำตอบที่ผิด และดูน่าเชื่อถือด้วย


examples/s07/03_aliasing_nyquist.py↗ — สูตรในสไลด์ก่อนเดินให้ดูบนบอร์ดจริงทีละขั้น อัตราสุ่มถูกตรึงไว้ที่ 40 Hz ตลอด เปลี่ยนเฉพาะความถี่ของคลื่นจริง · ภาพนี้คือขั้นแรกซึ่งเป็นตัวคุม f จริง = 5 Hz ยังต่ำกว่า Nyquist ที่ 20 Hz อยู่มาก ความถี่ที่เครื่อง "เห็น" จึงเป็น 5 เท่ากัน และเส้นทับกันสนิท — ผีจะโผล่ก็ต่อเมื่อกดเดินหน้าจนความถี่จริงเกิน 20 Hz · ภาพหน้าจอจริงจากบอร์ด Eva Kit บันทึกโดยผู้สอน · ขวา — หน้าจอที่ examples/lvgl_ports/sec3_sensor_viz/eva/ex16_spectrum_analyzer.py สร้างบน Emulator (โปรไฟล์ Eva): สเปกตรัมจาก dsp.fft_mag ของจริงใน C — sine 1 kHz ยอดเดี่ยวที่ bin 5 อ่าน Dominant 937 Hz เพราะความละเอียดต่อ bin คือ 48000/256 = 187.5 Hz (firmware 2026-08-20 ขึ้นไป)การสุ่มไม่ทันคือความผิดพลาดที่ ไม่ส่งเสียง — ต้องรู้ล่วงหน้าว่าสัญญาณของเราเร็วแค่ไหน
int() ก่อน set_next — และราคาที่ต้องจ่ายChart เก็บค่าเป็น จำนวนเต็ม เท่านั้น แต่ motion() คืนทศนิยม เช่น 9.78
chart.set_next(0, int(ax)) # ถูก
chart.set_next(0, ax) # TypeError: can't convert float to int
ราคาที่ต้องจ่ายคือ int() ตัดทศนิยมทิ้ง ไม่ใช่ปัดเศษ — 0.9 เป็น 0 และ −0.9 ก็เป็น 0 กราฟจึงหยาบเป็นขั้นละ 1 m/s² นี่คือ quantization ตัวเดียวกับคาบ 5 แค่คราวนี้เราเป็นคนสร้างเอง
สูตร map ช่วงจริงลงช่วงแกน — วิธีแก้ที่ใช้ในงานจริง
ไทย: แกน Y ของ Chart เป็นจำนวนเต็ม ต้อง map ช่วงค่าจริงลงช่วงแกนก่อน ไม่งั้นกราฟแบนหรือทะลุขอบ
chart = ui.Chart(..., min=-2000, max=2000) # หน่วยกลายเป็น 0.01 m/s²
chart.set_next(0, int(ax * 100)) # 9.78 -> 978
ตัวเลขจากบอร์ดนี้: ช่วง ±20 กับ int() ตรง ๆ ได้ความละเอียด 1 m/s² · คูณ 100 แล้วขยายช่วงเป็น ±2000 ได้ 0.01 m/s² — ละเอียดขึ้น 100 เท่าโดยไม่แตะเซนเซอร์เลย

ตัวเลขบนแกน Y ไม่จำเป็นต้องเป็นหน่วยจริง ขอแค่ เรากับคนอ่านกราฟรู้ตรงกัน ว่ามันคูณอะไรไว้
set_next() ไม่ได้ "วาดกราฟ"ไทย: หน้าจอเรากว้าง 10 วินาที ของเก่ากว่านั้นถูกดันตกขอบไปแล้ว · อยากเห็นย้อนหลังนานขึ้นมีสองปุ่มให้หมุน — ยืดคาบให้ห่างขึ้น หรือ เพิ่มจำนวนจุด ด้วย ch.prop(ui.PROP_CHART_POINTS, n) ตั้งได้ 10-400 (firmware 2026-08-20 ขึ้นไป · รุ่นก่อนหน้า 50 ตายตัว) แต่ทุกจุดที่เพิ่มคือข้อความ IPC ที่ต้องส่งเพิ่มเวลาวาดทั้งเส้น คาบที่ห่างขึ้นจึงยังเป็นปุ่มที่ถูกกว่าเสมอ
จอเรามี 50 ช่องตามค่าปริยาย — คาบการสุ่มคือตัวกำหนดว่าเราเห็นอดีตย้อนหลังได้กี่วินาที
โค้ด Python อยู่บน CM33 กราฟถูกวาดโดย CM55 chart.set_next() ไม่ได้วาดทันที มันแค่ ฝากคำสั่งลงคิว IPC แล้วกลับมาทำงานต่อ
ฝั่ง CM55 มีตัวจับเวลาคอยหยิบคำสั่งจากคิวมาทำ และมันมี สองความเร็ว
| โหมด | คาบของตัวจับเวลา | หยิบได้ต่อครั้ง | อัตราการระบายจริง |
|---|---|---|---|
| ปกติ (idle) | 200 ms | 16 คำสั่ง | 80 คำสั่ง/วินาที |
| เร่ง (fast) | 5 ms | 16 คำสั่ง | 3,200 คำสั่ง/วินาที |
และนี่คือกับดัก — คำสั่งที่ ปลุกโหมดเร่ง มีอยู่ชุดหนึ่ง คำสั่งที่ ไม่ปลุก ก็มีอีกชุด
| ปลุกโหมดเร่ง | ไม่ปลุกโหมดเร่ง |
|---|---|
สร้าง widget · .text() · .pos() · .size() · .color() |
.value() · .set_next() · .show() / .hide() |
โหมดเร่งจะกลับเป็นปกติเองหลังเงียบไป 500 ms
BENTO-TESAIoT-libraries/claw/common/modules/ipc_ui/ipc_ui.c — IPC_UI_TIMER_MS=200, IPC_UI_FAST_TIMER_MS=5, IPC_UI_MAX_PER_TICK=16, IPC_UI_FAST_TIMEOUT_MS=500 และรายการ opcode ที่เรียก ui_arm_fast_mode()
set_next()เป็นคำสั่งที่ ไม่ปลุก โหมดเร่ง — กราฟที่วิ่งอยู่ตัวเดียวบนจอ จึงเป็นกรณีที่ช้าที่สุดพอดี
รู้กลไกแล้ว ทางแก้เหลือบรรทัดเดียว — ให้ทุกลูปมีคำสั่งที่ปลุกโหมดเร่งอย่างน้อยหนึ่งคำสั่ง และตัวที่เป็นธรรมชาติที่สุดคือป้ายสถานะที่เราอยากเห็นอยู่แล้ว
rec_msg = "กำลังบันทึก" # เปลี่ยนเฉพาะตอนกดปุ่ม
while True:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
chart.set_next(0, int(ax)) # ไม่ปลุกโหมดเร่ง
chart.set_next(s_ay, int(ay)) # ไม่ปลุก
chart.set_next(s_az, int(az)) # ไม่ปลุก
led_rec.value(1) # ไม่ปลุก - .value() ทุกตัวไม่ปลุก
lbl_rec.text(rec_msg) # ← ปลุก และค้างไว้อีก 500 ms
ui.poll()
time.sleep_ms(PERIOD_MS)
ทำไมมันได้ผลกับลูป 200 ms ของเรา — โหมดเร่งค้างอยู่ 500 ms หลังคำสั่งสุดท้าย ลูปเราหมุนทุก 200 ms ซึ่งสั้นกว่า 500 ms ตัวจับเวลาจึงไม่มีโอกาสกลับเป็นโหมดปกติเลยตลอดการรัน
กฎนี้ชนกับกฎ "ตัวเลขเปลี่ยนไม่เกินวินาทีละครั้ง" พอดี และทางออกอยู่ตรงกลาง: ส่ง .text() ทุกรอบ แต่ส่ง ข้อความเดิม เฉลยของคาบนี้จึงเก็บข้อความสถานะไว้ในตัวแปร rec_msg ซึ่งเปลี่ยนเฉพาะตอนกดปุ่ม แล้วส่งซ้ำทุกรอบ · เฟิร์มแวร์เทียบข้อความเก่ากับใหม่ก่อนวาด ถ้าเท่าเดิมมันไม่วาดซ้ำ เราจึงจ่ายแค่ค่าส่งข้ามคอร์ ไม่ได้จ่ายค่าวาด และไม่มีตัวเลขไหนบนจอวิ่งเร็วกว่าที่คนอ่านทัน
คำสั่งที่ปลุกโหมดเร่งได้มีสี่กลุ่ม คือสร้าง widget · .text() · .pos() / .size() / .color() · และคำสั่งของ collection อย่าง .cell() .add_row() .prop() — ส่วน .value() ทั้งหมด ไม่ว่าจะเป็นแถบ ไฟ หรือ set_next() ของกราฟ ไม่ปลุก (ตรวจจาก ipc_ui.c โดยตรง)
อย่าตีความเกินกว่านี้ — โหมดเร่งไม่ได้ทำให้เซนเซอร์อ่านเร็วขึ้น และไม่ได้ทำให้ลูป Python เร็วขึ้น มันแค่ทำให้ คำสั่งที่เราส่งไปแล้ว ถูกวาดออกจอเร็วขึ้น
ลูปที่อัปเดตกราฟอย่างเดียวคือลูปที่ช้าที่สุด — ใส่ป้ายสถานะไว้ในลูปเดียวกันเสมอ ได้ทั้งความลื่นและได้ตัวเลขให้คนอ่าน
chart = ui.Chart(x=24, y=52, w=292, h=144, min=-20, max=20, color=COL_AX)
s_ay = chart.add_series(COL_AY) # คืนค่าเป็น "หมายเลข series"
s_az = chart.add_series(COL_AZ)
...
chart.set_next(0, int(ax)) # เติมจุดใหม่ให้ series 0
chart.set_next(s_ay, int(ay)) # เติมจุดใหม่ให้ series ที่เพิ่งเพิ่ม
ข้อที่หนึ่ง ตอนสร้าง ui.Chart มันสร้าง series 0 ให้อัตโนมัติ โดยใช้สีจาก color= เราจึงไม่ต้องเรียก add_series สำหรับเส้นแรก (COL_AX COL_AY COL_AZ คือ 0x4A9EFF 0x8E7BFF 0x2FB6A8 จากจานสีเส้นข้อมูล — บรรทัดจริงจาก solution_codes/s07_accel_chart.py↗)
ข้อที่สอง .add_series(color) คืนค่าเป็นตัวเลข ให้เก็บใส่ตัวแปรไว้เสมอ ถ้าไม่เก็บ ก็ไม่มีทางเติมข้อมูลให้เส้นนั้นได้อีกเลย ค่าที่คืนมาคือ 1 แล้ว 2 แล้ว 3 ตามลำดับที่เรียก เพราะ 0 ถูกจองไปแล้วตั้งแต่ตอนสร้าง เรียกครั้งที่สี่จะได้ RuntimeError: ui: add_series failed (max 4)
ข้อที่สาม min= กับ max= คือช่วงแกน Y ตั้งครั้งเดียวตอนสร้าง ไม่มี autoscale
ข้อที่สี่ set_next() เป็นคำสั่งแบบยิงแล้วลืม ส่ง idx ที่ไม่มีอยู่จริง เช่น set_next(3, ...) ทั้งที่มีแค่สามเส้น จะไม่มี error อะไรทั้งสิ้น ข้อมูลนั้นหายไปเฉย ๆ — ต่างจากค่าที่เป็นทศนิยม ซึ่งได้ TypeError ตั้งแต่ฝั่ง Python
add_seriesคืน "ชื่อเรียก" ของเส้น — ไม่เก็บไว้ เท่ากับสร้างเส้นที่เราส่งข้อมูลไปหาไม่ได้
ui.Chart ไม่มี autoscale ค่าที่เกินช่วงจะถูกกดให้ติดขอบ ดูเผิน ๆ เหมือนสัญญาณอิ่มตัว ทั้งที่จริงคือเราตั้งกรอบไว้แคบไป
ความเร่งบนบอร์ดนี้: วางนิ่ง Z ราว 9.8 m/s² · X, Y ราว 0 · เขย่าแรงพุ่ง 15-20 · กระแทกโต๊ะทะลุ 30
| ช่วงที่ตั้ง | ผลที่ได้ |
|---|---|
| −2 ถึง +2 | เห็นการสั่นเล็ก ๆ ชัด แต่แกน Z ติดขอบบนตลอด ใช้ไม่ได้ |
| −20 ถึง +20 | เห็นทั้งแรงโน้มถ่วงและการเขย่า สมดุลที่สุดสำหรับคาบนี้ |
| −100 ถึง +100 | ไม่มีอะไรตกขอบ แต่เส้นแบนติดกลางจอ |
สีคือภาษาที่คนอ่านกราฟใช้ร่วมกัน — สามเส้นนี้หยิบจาก "จานสีเส้นข้อมูล" ของหลักสูตร ไม่ใช่ แดง/เขียว/น้ำเงิน ตามธรรมเนียมเดิม
| แกน | สีที่เห็นบนจอ | ค่าคงที่ในโค้ด |
|---|---|---|
| X | ฟ้า | 0x4A9EFF — เส้นที่ 1 |
| Y | ม่วง | 0x8E7BFF — เส้นที่ 2 |
| Z | เขียวน้ำทะเล | 0x2FB6A8 — เส้นที่ 3 |
ทำไมไม่ใช้ แดง/เขียว/น้ำเงิน — จานสีชุดนี้ไม่มีสีสถานะปนอยู่เลยสักตัว โดยตั้งใจ หน้าจอนี้มีไฟเตือนอยู่ด้วย และ §S7.7.2 ห้ามใช้สีของการแจ้งเตือนกับอย่างอื่นในหน้าจอเดียวกัน เส้น X สีแดงที่แปลว่า "แกน X" เฉย ๆ จะแย่งความหมายของแดงที่แปลว่า "ต้องรีบดู" ไปจนหมด
หน้า Sensor Dashboard ของเฟิร์มแวร์ ที่เห็นในสไลด์แรกยังใช้ แดง/เขียว/น้ำเงิน อยู่ ของเราจึงไม่เหมือนของมัน — ตั้งใจให้ไม่เหมือน นี่คือตัวอย่างจริงของหน้าจอที่เขียนก่อนจะมีกฎข้อนี้
clamp ก่อนส่ง (
max(-20, min(20, ax))) ทำให้เรา รู้ตัว ว่ากำลังตัดข้อมูล · ถ้าคนดูต้องถามว่า "เส้นไหนคือแกนอะไร" กราฟยังไม่เสร็จ
เวลาเห็นอะไรน่าสนใจวิ่งผ่านกราฟ เราอยากหยุดดู แต่ ห้ามหยุดลูป เด็ดขาด
เพราะถ้าลูปหยุด ui.poll() ก็หยุดตาม กฎเหล็กข้อแรกเล่นงานทันที — จอซ่อน widget ภายในราวสองวินาที ปุ่มเริ่มบันทึกที่จะกดก็หายไปด้วย กลายเป็นหยุดถาวรที่กู้ไม่ได้
running = True
while True:
for ev in ui.poll(): # ต้องเรียกทุกลูป ไม่ว่าจะหยุดหรือไม่
h = ev.get('handle')
if h == start_id:
running = True # ปุ่มเริ่ม มีหน้าที่เดียว
led_rec.value(1)
elif h == stop_id:
running = False # ปุ่มหยุด มีหน้าที่เดียว
led_rec.value(0)
if running:
chart.set_next(0, int(ax)) # เติมข้อมูลเฉพาะตอนเดิน
time.sleep_ms(200) # หน่วงเท่าเดิมเสมอ
ui.Led เป็นคนบอกสถานะปัจจุบัน ไม่ใช่ให้ปุ่มบอก · กฎเดียวกันนี้ใช้กับปุ่มเปิด-ปิดของอุปกรณ์จริงทุกชนิดหยุดข้อมูล ไม่ใช่หยุดลูป — ลูปที่ยังหมุนอยู่คือสิ่งเดียวที่ทำให้จอยังมีชีวิต
เราสั่ง time.sleep_ms(200) แต่ลูปหนึ่งรอบไม่ได้ใช้เวลา 200 ms พอดี เพราะยังมีเวลาอ่านเซนเซอร์ ส่งข้าม IPC และอัปเดตป้ายอีกหลายใบ
ไทย: คาบจริง = เวลาที่หน่วง + เวลาที่ทำงาน · ตัวเลขที่ควรอ่านได้ (วัดบน Eva Kit · Dev Kit อ่าน IMU คนละเส้นทาง ตัวเลขอาจต่าง ให้จดของทีมเอง): 205-215 ms ถ้าพุ่งถึง 300 ms แปลว่างานในลูปหนักกว่าที่คิด และ ตกจาก 5 Hz เหลือ 3.3 Hz โดยที่โค้ดไม่บอกเราสักคำ
now = time.ticks_ms() # นาฬิกามิลลิวินาทีของระบบ
dt = time.ticks_diff(now, last_ms) # ผลต่างจากรอบที่แล้ว
last_ms = now
ทำไมต้อง ticks_diff แทน now - last_ms — ticks_ms() เป็นตัวนับที่ วนกลับไปเริ่มใหม่ เมื่อชนเพดาน ถ้าลบเองในจังหวะที่มันวนพอดี จะได้ค่าติดลบมหาศาลแบบไม่มีปี่มีขลุ่ย ส่วน ticks_diff รู้เรื่องการวนนี้และคืนค่าที่ถูกต้องเสมอ
โปรแกรมที่วัดตัวเองได้ ดีบักง่ายกว่าโปรแกรมที่ต้องเดาเสมอ
คาบ 4 เราเจอสี่เมธอดนี้กับกล่องสี่เหลี่ยมใบเดียว วันนี้ขอย้ำอีกครั้ง เพราะ Chart ก็รับคำสั่งชุดเดียวกันนี้ และคนส่วนใหญ่ไม่คิดถึงมันตอนออกแบบหน้าจอที่มีกราฟ
w.pos(x, y) # ย้ายไปพิกัดใหม่ - สองอาร์กิวเมนต์ ขาดไม่ได้
w.size(cw, ch) # เปลี่ยนขนาด - สองอาร์กิวเมนต์เหมือนกัน
w.hide() # ยังอยู่ แต่ไม่วาด
w.show() # กลับมาวาด
ทั้งสี่ตัวใช้ได้กับ widget ทุกชนิด ไม่เว้นแม้แต่ Chart, Panel หรือ Compass เพราะมันอยู่ในตารางเมธอดชุดเดียวกันหมด
สามข้อที่ต้องรู้ก่อนใช้
w.pos(10) ได้ TypeError เรื่องจำนวนอาร์กิวเมนต์ ส่วน w.pos(10.5, 20) ได้ TypeError: can't convert float to int เหมือน set_next().x() ไม่มี .y() ไม่มีอะไรถามตำแหน่งปัจจุบันได้เลย ย้ายไปไหนแล้ว เราต้องจำเอง ในตัวแปรฝั่ง Python.hide() ไม่ใช่ .delete() — ของที่ซ่อนยัง กินช่องในตาราง 64 ตัวอยู่เต็ม ๆ ส่วน .delete() คืนช่องให้ แต่ตัวแปร Python ยังชี้ไปที่ handle เดิมที่ตายแล้ว สั่งอะไรต่อก็เงียบ.pos() กับ .size() ปลุก โหมดเร่ง ส่วน .show()/.hide() ไม่ปลุก หน้าที่สลับการ์ดด้วย .hide() ล้วน ๆ จึงตอบช้าแบบเดียวกับกราฟที่มีแต่ set_next() วิธีแก้เดียวกัน: มีป้ายสถานะ "ตอนนี้อยู่หน้าไหน" อัปเดตไปด้วย · ของจริง: examples/s04/07_find_move_hide_delete.py↗ นับ ui.list() ให้ดูว่าซ่อนแล้วตัวเลขไม่ลด แต่ลบแล้วลดย้ายของที่มีอยู่แล้วถูกกว่าสร้างใหม่เสมอ — ทั้งเรื่องงบ 64 ช่อง และเรื่องเวลาที่ CM55 ต้องใช้วาด
ui — ตารางอ้างอิง กับกับดักประจำตัวของแต่ละตัวคาบ 1-6 เราใช้ไปแล้วห้าหกตัว วันนี้เพิ่ม Chart ส่วนที่เหลือมีอยู่จริงและเรียกได้ทันที ตารางนี้ไม่ต้องท่อง แต่ต้องรู้ว่ามีอะไรอยู่ จะได้ไม่ไปเขียนเองในสิ่งที่เฟิร์มแวร์มีให้แล้ว
| widget | สร้างด้วยอะไร | สั่งงานด้วยอะไร | กับดักที่ต้องรู้ |
|---|---|---|---|
Chart |
min= max= color= |
.add_series(สี) → 1,2,3 · .set_next(idx, ค่า) |
series 0 มาฟรี · เพดาน 4 เส้น · เส้นละ 50 จุดปริยาย ตั้ง 10-400 ผ่าน .prop(ui.PROP_CHART_POINTS, n) (fw 2026-08-20 ขึ้นไป) · idx เกินถูกทิ้งเงียบ |
DotMatrix |
cols= rows= (ไม่ใช่ min/max — สองตัวนั้นถูกเขียนทับ) |
.set_pixels(บัฟเฟอร์) |
1 บิตต่อหนึ่งดอก MSB ก่อน ไหลต่อกันข้ามไบต์ ไม่มีการเติมให้ครบไบต์ท้ายแถว · เพดาน 16×16 ดอก และ 126 ไบต์ต่อครั้ง เกินกว่านั้นถูกตัดเงียบ · สีดอกตายตัว color= ไม่มีผล |
Image |
text= คือ ชื่อไอคอน ไม่ใช่ข้อความ |
.icon(ชื่อ) · .set_image(RGB565) |
ชื่อผิดตอนสร้าง = RuntimeError แต่ชื่อผิดใน .icon() = เงียบสนิท · ผืนว่างถูกบีบไว้ที่ 48×48 |
Arc |
min= max= value= |
.value(n) ตั้ง · .value() อ่านกลับได้จริง |
เป็นของที่ คนลากได้ ส่ง value_changed เข้า poll() ด้วย ถ้าไม่อยากให้ลาก ใช้ Bar |
Spinner |
มีแค่ x y w h |
ไม่มีอะไรให้สั่ง นอกจาก .show() / .hide() |
min max value text ถูกเมินหมด · ไม่ส่ง w มาจะกลายเป็น 80×80 |
Dropdown |
text="แดง\nเขียว\nน้ำเงิน" — ขึ้นบรรทัดใหม่คั่นตัวเลือก |
.value() คืน หมายเลขที่เลือก · poll() ส่ง value_changed |
value= คือ ขนาดฟอนต์ ไม่ใช่ตัวที่เลือก · ตัวเลือกทั้งชุดรวมกันต้องไม่เกิน 126 ไบต์ |
Textarea |
text= ข้อความตั้งต้น · value= ขนาดฟอนต์ |
.text("...") เขียนทับได้ |
อ่านกลับไม่ได้ .text() แบบไม่ใส่อาร์กิวเมนต์คืน None เสมอ และมันไม่ส่ง event เลยสักชนิด — เป็นช่องแสดงผล ไม่ใช่ช่องกรอก |
Compass |
w= คือ เส้นผ่านศูนย์กลาง h ถูกเมิน |
.value(องศา) 0 คือทิศเหนือ |
รับจำนวนเต็ม · .value() อ่านกลับได้ 0 เสมอ จำองศาไว้เองฝั่ง Python |
Panel |
color=พื้น min=สีขอบ max=รัศมี value=ความหนา |
— | ไม่ได้เป็น "พ่อ" ของ widget ที่วางทับ ต้องสร้างก่อนเสมอ ไม่งั้นมันบังของอื่นหมด |
Seg7 |
w h color |
.text("123") |
.value(n) ได้จำนวนเต็ม ถ้าต้องการทศนิยมต้อง .text() |
ชื่อไอคอนที่มีให้ใช้ มีอยู่ 16 ชื่อพอดี ผิดจากนี้ไม่ขึ้น: heart star flag trophy skull arrow_up arrow_down arrow_left arrow_right check cross smiley car boat plane home
.set_image() มีจริงแต่เราจะไม่ใช้ในคาบ — ภาพ 48×48 แบบ RGB565 คือ 4,608 ไบต์ ซึ่งต้องหั่นส่งข้าม IPC ถึง 38 ครั้ง โดยมีการหน่วง 200 ไมโครวินาทีคั่นทุกครั้ง ราคานั้นแพงเกินสำหรับของที่อัปเดตในลูป ใช้ .icon() แทนถ้าต้องการรูปสัญลักษณ์
ของที่สองบอร์ดมีไม่เท่ากัน — ui.Sprite และค่าคงที่ SPR_* ทั้งชุด ไม่ถูกคอมไพล์เข้ามาในเฟิร์มแวร์ของ Eva Kit (ธง ENABLE_GAME_SPRITES ปิด) เขียนถึงมันบน Eva จะได้ AttributeError · บน TESAIoT Dev Kit ธงนี้เปิดอยู่ (Makefile.micropython ของโปรเจกต์นั้น ตั้งแต่ 2026-08-20) ui.Sprite จึงมีจริง — โค้ดที่ต้องรันได้ทั้งสองบอร์ดจึงห้ามพึ่งมัน หรือต้องตรวจ hasattr(ui, "Sprite") ก่อน · ส่วน ui.tone() กับ ui.sfx() มีทั้งสองบอร์ด เพราะมีชิปเสียงเหมือนกัน — examples/s08/05_door_open_switch.py↗ ใช้ ui.tone() อยู่จริง
ตัวไหนมีของให้ลองอยู่ที่ไหน — DotMatrix + .set_pixels() ที่ examples/s07/02_fft64_two_tones.py↗ (สเปกตรัมเป็นตารางไฟ) และ examples/s01/14_the_board_hears_you.py↗ · Image .icon() และ Spinner ที่ examples/s01/15_one_number_many_faces.py↗ · Dropdown กับ Textarea ที่ examples/s04/08_dropdown_textarea.py↗ · เหลือ .set_image() ตัวเดียวที่ไม่มีไฟล์ไหนใช้ ด้วยเหตุผลย่อหน้าบน
รู้ว่ามีอะไรอยู่ในกล่องเครื่องมือ สำคัญกว่าท่องวิธีใช้ทุกตัว — เปิดตารางนี้ตอนคิดไม่ออกว่าจะแสดงค่านี้ด้วยอะไรดี
สไลด์ "ย้าย ย่อ ซ่อน" หยิบมาสี่ตัว สไลด์นี้ปิดบัญชี — เมธอดของอ็อบเจกต์ widget มีสิบสี่ชื่อ ไม่มีชื่อที่สิบห้า
เก้าตัวที่ใช้ได้กับ widget ทุกชนิด รวมทั้ง Chart เพราะทุกชนิดใช้ตารางเมธอดชุดเดียวกัน
| เมธอด | ทำอะไร | คาบนี้ใช้ตรงไหน |
|---|---|---|
.id() |
เลขประจำตัว 0-63 ที่ตรงกับ ev['handle'] |
ใช้ — แยกว่าปุ่มเริ่มหรือปุ่มหยุดถูกกด |
.text() / .text("...") |
อ่านคืน None เสมอ / เขียนข้อความทับ |
ใช้ทุกรอบ — ป้ายค่าและป้ายสถานะ · และเป็นตัวที่ทำให้กราฟลื่นขึ้นตามสไลด์ "ทำไมกราฟช้า" |
.value() / .value(n) |
อ่าน / สั่งค่า มีหกชนิดเท่านั้นที่ตอบได้จริง | ไม่ใช้ในคาบนี้ · Chart ไม่ใช่หนึ่งในหกชนิดนั้น |
.pos(x, y) .size(w, h) |
ย้าย / เปลี่ยนขนาดหลังสร้างแล้ว | ใช้ตอนจัดหน้า · สองตัวนี้ ปลุกโหมดเร่ง ส่วน show/hide ไม่ปลุก |
.color(0xRRGGBB) |
เปลี่ยนสี | ใช้เป็นช่องรายงานสถานะได้ ตาอ่านสีเร็วกว่าตัวอักษร |
.show() / .hide() |
ซ่อน-แสดงโดยไม่ลบ | ของที่ซ่อนอยู่ ยังกินโควตา 64 เต็ม ๆ |
.delete() |
ลบจริง คืนโควตาหนึ่งช่อง | ตัวแปร Python ยังชี้ไปที่ handle ที่ตายแล้ว สั่งต่อก็เงียบ |
สองตัวที่เป็นของ Chart ล้วน ๆ — หัวใจของคาบนี้
| เมธอด | ทำอะไร · กับดัก |
|---|---|
.add_series(สี) |
คืนหมายเลขเส้น 1, 2, 3 ตามลำดับที่เรียก · เพดานสี่เส้น เรียกเกินได้ RuntimeError |
.set_next(idx, ค่า) |
ยิงแล้วลืม · idx ผิดถูกทิ้งเงียบ · ค่าทศนิยมได้ TypeError ตั้งแต่ฝั่ง Python |
สามตัวที่เป็นของฝั่งภาพ — คาบนี้ไม่ได้ใช้ในโครงหลัก
.icon(ชื่อ) เปลี่ยนไอคอนจาก 16 ชื่อที่มี · .set_image(RGB565) วางภาพ 48×48 · .set_pixels(บัฟเฟอร์) ของ DotMatrix — ตัวหลังโผล่จริงในตัวอย่างเสริม examples/s07/02_fft64_two_tones.py↗
และฝั่งตัวสร้างมีสิบหกตัว ตารางอ้างอิงข้างบนกางไว้สิบชนิด อีกหกตัวที่เหลือคือของที่เราใช้มาตั้งแต่คาบ 1-6 อยู่แล้ว: Label Button Switch Slider Checkbox Bar — บัญชีเต็มของทั้งโมดูล ui อยู่ในคาบ 4

กราฟเวลาที่เราทำวันนี้ตอบได้ว่า "ค่าเปลี่ยนไปยังไงตามเวลา" แต่ตอบไม่ได้ว่า "การสั่นนี้ประกอบด้วยความถี่อะไรบ้าง"
ภาพขวาคือข้อมูลจริงจากงานเฝ้าระวังตลับลูกปืน แถวบนเป็นโดเมนเวลา แถวล่างเป็นสเปกตรัมของข้อมูล ชุดเดียวกันเป๊ะ — กราฟเวลาของตัวปกติกับตัวเสียดูคล้ายกันมาก แต่สเปกตรัมแยกออกทันที
นี่คือสาเหตุที่งาน predictive maintenance ในโรงงานทำ FFT ก่อนตัดสินใจ ไม่ได้ดูกราฟเวลาอย่างเดียว สเปกตรัมคือที่ที่ความต่างซ่อนอยู่ ทั้งที่กราฟเวลาบอกไม่ได้
เชื่อมกับวันนี้: ที่ Hz เราทำ FFT ที่มีประโยชน์ไม่ได้ เพราะเห็นได้แค่ถึง 2.5 Hz ส่วนความสั่นของเครื่องจักรจริงอยู่หลักกิโลเฮิรตซ์ — อัตราแบบนั้นลูป Python เอื้อมไม่ถึง ต้องให้ เฟิร์มแวร์เป็นคนสุ่มตัวอย่าง แล้วส่งค่าที่ยุบแล้วขึ้นมาให้ เหมือนที่โมดูล mic ทำในคาบ 8
โมดูล dsp มี FFT ในตัวแล้ว: dsp.fft_mag(ชุด, n=256) คืนสเปกตรัม n/2 ช่อง จบใน C ราวหนึ่งมิลลิวินาที (เพิ่ม 2026-08-20 — firmware รุ่นก่อนหน้ายังไม่มี · ulab ยังไม่มีเช่นเดิม) — ข้างในของมันคือ radix-2 ที่สไลด์อ่านเสริมท้ายเด็คแกะให้ดูทั้งตัว


สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
ไดรเวอร์ BMI270 และการอ่านหกแกนจากการอ่านครั้งเดียว (Eva: snapshot ของคอร์จอ · Dev Kit: ล็อกบัสของ CM33) · การวาดกราฟ เส้น กริด และการเลื่อนบัฟเฟอร์บน CM55 · การส่งคำสั่งข้าม IPC พร้อมโหมดเร่ง · การรับสัมผัสจากจอแล้วแปลงเป็น event · นาฬิกาของระบบ
สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่า สุ่มถี่แค่ไหน · ช่วงแกน Y เท่าไร · เส้นไหนสีอะไร · ให้ผู้ใช้หยุดดูได้ไหม · และ จะรู้ได้ยังไงว่าลูปเรายังทันจริง
ห้าข้อนี้ไม่มีใน API เล่มไหน มันคือการออกแบบระบบวัด ซึ่งเป็นงานของวิศวกร ไม่ใช่งานของไลบรารี
ไลบรารีวาดเส้นให้เราได้ แต่ตัดสินใจแทนเราไม่ได้สักข้อ
ก่อนต่อเซนเซอร์จริง ลองป้อนสัญญาณที่เรารู้คำตอบล่วงหน้าเข้ากราฟดูก่อน วิธีนี้แยกได้ชัดว่า ถ้ากราฟเพี้ยน มันเพี้ยนที่กราฟหรือเพี้ยนที่เซนเซอร์
import ui, time, math
ui.screen()
ch = ui.Chart(x=40, y=60, w=700, h=250, min=-100, max=100, color=0x4A9EFF)
lbl = ui.Label("i = 0", x=40, y=330) # ปลุกโหมดเร่งทุกรอบ
for i in range(200):
val = int(90 * math.sin(i * 0.15)) # ไซน์
# val = 80 if (i // 8) % 2 == 0 else -80 # สี่เหลี่ยม
# val = (i % 20) * 9 - 90 # ฟันเลื่อย
ch.set_next(0, val)
lbl.text("i = {}".format(i))
ui.poll()
time.sleep_ms(100)
ลองสลับบรรทัดที่คอมเมนต์ไว้ทีละแบบ แล้วดูว่ารูปคลื่นบนจอตรงกับที่คิดไหม จากนั้นเปลี่ยน 0.15 เป็น 0.6 — ไซน์จะเริ่มดูไม่เหมือนไซน์ เพราะเราสุ่มไม่ทันมันแล้ว นั่นคือ aliasing ในสิบบรรทัด
สังเกตบรรทัด lbl.text(...) ที่เพิ่มเข้ามา — นั่นคือกฎจากสไลด์ก่อนหน้า ลองลบมันออกแล้วรันใหม่ กราฟจะเดินช้าลงอย่างเห็นได้ชัดทั้งที่ sleep_ms เท่าเดิม

ทดสอบด้วยสัญญาณที่เรารู้คำตอบก่อนเสมอ แล้วค่อยเอาของจริงเข้าไป
# --- ท่าที่ 1: เตรียมเซนเซอร์และหน้าจอให้พร้อม ---
# ไม่มี sensors.init() ทั้งสองบอร์ด (Eva: ได้ OSError / Dev Kit: ไม่จำเป็น)
try:
sensors.bmi270.motion() # อ่านทิ้งหนึ่งครั้ง ให้การรอไปเกิดตรงนี้
except OSError:
print("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ในลูป")
ui.clear()
time.sleep_ms(200)
ไม่ต้องเรียก sensors.init() ทั้งสองบอร์ด — บน Eva Kit เรียกแล้วได้ OSError ทันที เพราะคอร์จอ (CM55) เป็นเจ้าของบัส I2C ตัวนั้น ค่าเซนเซอร์มาจากภาพสแกนที่ CM55 เก็บไว้ให้ · บน Dev Kit CM33 อ่าน IMU ตรงจาก I2C เอง และเฟิร์มแวร์ปลุกมันไว้ตั้งแต่บูต · ทั้งสองทางจึงเรียก sensors.bmi270.motion() ได้ตรง ๆ โค้ดชุดเดียวกันรันได้ทั้งคู่
การอ่านครั้งแรกทำหน้าที่แทนการหน่วงเวลา — หลังรีเซ็ต การอ่านครั้งแรกอาจต้องรอเซนเซอร์ตอบ (Eva วัดได้: CM55 เริ่มตอบที่ราว 13 วินาที การอ่านแรกรอได้ถึง 16 วินาที · Dev Kit ยังไม่ได้วัด) ครั้งต่อ ๆ ไปไม่เกิน 1 วินาที เราอ่านทิ้งหนึ่งครั้ง ก่อนสร้างกราฟ เพื่อไม่ให้ค่าชุดแรกที่ยังไม่นิ่งไปค้างในบัฟเฟอร์กราฟนาน 10 วินาที · ui.clear() ล้าง widget ของโปรแกรมก่อนหน้าออกให้หมด เริ่มจากสถานะที่รู้แน่
จำกฎเหล็กข้อที่สี่: การใช้
ui.*ครั้งแรกจะหยุด sensor auto-task — เลือกใช้uiเมื่อไร ก็ต้องรับหน้าที่อ่านเซนเซอร์เองในลูปเมื่อนั้น
# --- ท่าที่ 2: กราฟหนึ่งใบ สามเส้น และตารางสรุปข้าง ๆ ---
chart = ui.Chart(x=24, y=52, w=292, h=144, min=-20, max=20, color=COL_AX)
s_ay = chart.add_series(COL_AY)
s_az = chart.add_series(COL_AZ)
tbl = ui.Table(x=332, y=52, w=436, h=280, cols=4, value=16)
tbl.col_width(0, 100) # กว้างพอสำหรับข้อความที่ยาวที่สุด
tbl.col_width(1, 108)
tbl.col_width(2, 108)
tbl.col_width(3, 108)
tbl.add_row("แกน", "ต่ำสุด", "สูงสุด", "ล่าสุด")
tbl.add_row("X", "-", "-", "-")
tbl.add_row("Y", "-", "-", "-")
tbl.add_row("Z", "-", "-", "-")
บรรทัดแรกได้กราฟ พร้อม series 0 สีฟ้า (COL_AX) มาเลย — จุดที่พลาดบ่อยที่สุดคือเผลอเรียก add_series ให้แกน X อีกครั้ง กลายเป็นสี่เส้นแล้วเส้นแรกไม่มีข้อมูลตลอดกาล · s_ay กับ s_az ได้ 1 และ 2 แต่ ไม่ควรพิมพ์ 1 กับ 2 ลงไปตรง ๆ สลับลำดับการสร้างเมื่อไร เลข hardcode จะชี้ผิดเส้นทันที
ui.Table ไม่ใช่ของซ้ำซ้อนกับกราฟ กราฟเก็บได้เท่าจำนวนช่อง (ปริยาย 50) ถาม "เมื่อครู่แรงสุดเท่าไร" จึงไม่มีคำตอบ ตารางเก็บค่าต่ำสุด-สูงสุดในตัวแปรฝั่ง Python · ระวังความกว้างคอลัมน์ แคบกว่าข้อความที่ยาวที่สุดเมื่อไร ข้อความขึ้นบรรทัดใหม่ แล้ว ทุกแถวสูงเป็นสองเท่า แถว Z หายใต้ขอบโดยไม่มี error · value= ของ ui.Table คือ ขนาดฟอนต์ในช่อง ไม่บอกจะได้ 20 · กราฟเตี้ยลงเหลือ 144 px เพื่อเหลือคอลัมน์ซ้ายใต้กราฟให้ไฟ คาบลูป และปุ่มสูง 88 px สองใบ
add_seriesคืนเลขอะไรมา ให้เก็บไว้ใช้ อย่าเดาเอง อย่าพิมพ์เอง
# --- ท่าที่ 3: ไฟบอกสถานะ และปุ่มเริ่มกับปุ่มหยุดที่แยกกัน ---
led_rec = ui.Led(x=24, y=212, w=48, h=48, color=COL_RUN, value=1)
lbl_rec = ui.Label("กำลังบันทึก", x=88, y=216, color=COL_TEXT, value=20)
rec_msg = "กำลังบันทึก" # ข้อความสถานะล่าสุด เปลี่ยนเฉพาะตอนกดปุ่ม
lbl_dt = ui.Label("คาบลูป -- ms", x=88, y=252, color=COL_DIM, value=20)
btn_start = ui.Button("เริ่มบันทึก", x=24, y=296, w=128, h=88,
color=0x30A46C, value=20)
btn_stop = ui.Button("หยุดบันทึก", x=184, y=296, w=128, h=88,
color=0x3A4150, value=20)
start_id = btn_start.id()
stop_id = btn_stop.id()
ui.poll()
running = True # ตัวแปรที่ต้องจำสถานะข้ามรอบ ต้องเกิดนอกลูป
ปุ่มสูง 88 px ตามขนาดเป้าสัมผัสของหลักสูตร — เดิมอยู่ในการ์ดสูง 64 px แล้วยื่นพ้นจอ กดไม่ได้ ตอนนี้วางเป็นสามชั้นในคอลัมน์ซ้ายใต้กราฟแทน · .id() คือหมายเลขประจำตัวของปุ่ม เก็บไว้เทียบกับ ev.get('handle') ตอนรับ event เพราะบนจอมีสองปุ่ม เราต้องรู้ว่าใครถูกกด · ปุ่มบนจอส่ง event ชนิด 'clicked' เท่านั้น (Switch ส่ง 'toggled' Slider ส่ง 'value_changed' — คาบ 4)
ปุ่มเริ่มกับปุ่มหยุดแยกกันคนละใบ ไม่ใช่ปุ่มเดียวสลับ ปุ่มที่เขียนว่า PAUSE บอกได้แค่ว่ากดแล้วจะเกิดอะไร ไม่ได้บอกว่าตอนนี้อยู่สถานะไหน คนที่เดินมาเห็นจอกลางคันจึงต้องเดา · ตัวที่บอกสถานะคือ ui.Led กับป้ายข้าง ๆ และ .value(0) ทำให้ไฟ หรี่ ไม่ใช่หาย โดยตั้งใจ · running = True ประกาศไว้ นอกลูป — ประกาศในลูปเมื่อไร มันถูกตั้งเป็น True ใหม่ทุกรอบ กดหยุดยังไงก็ไม่หยุด เป็นบั๊กที่หาไม่เจอง่าย ๆ เพราะโค้ดดูถูกทุกบรรทัด
# --- ท่าที่ 4: อ่านหกแกนจาก snapshot ชุดเดียว แล้วป้อนสามเส้น ---
if running:
try:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
except OSError:
pass # อ่านพลาดหนึ่งรอบ ใช้ค่าเดิมไปก่อน
chart.set_next(0, int(ax))
chart.set_next(s_ay, int(ay))
chart.set_next(s_az, int(az))
note(0, ax) # จำต่ำสุด-สูงสุดไว้ให้ตาราง
note(1, ay)
note(2, az)
motion() คืนหกค่าในการอ่านครั้งเดียว เราใช้แค่สามค่าแรก แต่ยังต้องรับให้ครบหกตัวแปร ไม่งั้น Python จะฟ้อง ValueError · ห่อ try ไว้เพราะอ่านพลาดหนึ่งรอบไม่ควรทำให้กราฟดับทั้งหน้า
ทำไมไม่เรียก acceleration() ที่คืนสามค่าพอดี — เพราะ motion() ให้หกแกน จากการอ่านครั้งเดียวกัน (Eva: snapshot ชุดเดียวจากคอร์จอ · Dev Kit: CM33 ล็อกบัส I2C ของตัวเองครั้งเดียว) ค่าจึงสอดคล้องกันและถามแค่รอบเดียว — เหตุผลเดียวกับคาบ 6 · เราส่ง int(ax) เข้ากราฟ แต่ส่ง ax ตัวเต็มให้ note() — กราฟรับได้แค่จำนวนเต็ม แต่คนอ่านอยากเห็นทศนิยม ค่าสุดขีดจึงเก็บด้วยความละเอียดเต็มไว้ฝั่ง Python แล้วค่อยจัดรูปตอนเขียนลงตาราง · note() ตั้งต้นด้วย None ไม่ใช่ 9999 เพราะ None แปลว่า "ยังไม่เคยวัด" ตรง ๆ — ค่าเดียวกันแปลงคนละแบบตามว่าปลายทางเป็นเครื่องหรือคน
# --- ท่าที่ 5: วัดคาบลูปจริงแล้วรายงาน ---
now = time.ticks_ms()
dt = time.ticks_diff(now, last_ms)
last_ms = now
sec = time.ticks_ms() // 1000 # ตัวเลขเปลี่ยนวินาทีละครั้ง
if sec != last_sec:
last_sec = sec
for i, v in enumerate((ax, ay, az)):
tbl.cell(i + 1, 1, cell(lo[i])) # แถว 0 คือหัวตาราง
tbl.cell(i + 1, 2, cell(hi[i]))
tbl.cell(i + 1, 3, "{:+.2f}".format(v))
lbl_dt.text("คาบลูป {} ms".format(dt))
time.sleep_ms(PERIOD_MS)
สามบรรทัดแรกคือแพตเทิร์นมาตรฐานของการวัดคาบ: จับเวลาตอนนี้ ลบด้วยเวลารอบที่แล้ว แล้วจำเวลาตอนนี้ไว้ใช้รอบหน้า
ค่า dt รอบแรกจะแปลก ๆ เพราะยังไม่มีรอบก่อนหน้าให้เทียบ เราจึงตั้งค่าตั้งต้นให้มันไว้ก่อนเข้าลูป
เส้นกราฟกับตัวเลขในตารางเดินคนละจังหวะโดยตั้งใจ เส้นกราฟถูกป้อนทุกรอบ คือ 5 ครั้งต่อวินาที เพราะตาคนอ่าน "รูปร่าง" ได้โดยไม่ต้องหยุดอ่าน ส่วนตัวเลขทศนิยมในตารางเขียนใหม่ ไม่เกินวินาทีละครั้ง — เลขที่วิ่งห้าครั้งต่อวินาทีคือเลขที่อ่านไม่ทัน · .cell(แถว, คอลัมน์, ข้อความ) เขียนทับช่องเดิม ไม่ได้เพิ่มแถวใหม่ ต่างจาก .add_row() ที่ใช้ตอนสร้าง
ตัวเลขที่ควรอ่านได้ (วัดบน Eva Kit · บน Dev Kit ยังไม่ได้วัด จดของทีมเอง) ราว 205-215 ms ถือว่าปกติดี ถ้าพุ่งถึง 300 ms ให้สงสัยว่าอัปเดตป้ายมากเกินจำเป็น ลองลดจำนวนป้ายที่อัปเดตทุกรอบลงแล้ววัดใหม่ — แต่ อย่าลดจนเหลือศูนย์ ไม่งั้นโหมดเร่งจะหลุด แล้วกราฟจะกระตุกแทน
ตัวเลขนี้คือ หลักฐาน ไม่ใช่ของประดับ — เอาไว้ตอบคำถามว่าลูปเรายังทันไหม
เห็นเส้นทางนี้แล้ว จะรู้ว่าเวลากราฟผิดปกติ ควรไปดูที่ จังหวะการส่ง ก่อนไปโทษเซนเซอร์
practise_codes/s07_accel_chart.py↗pass ไล่จากบนลงล่างทีละจุด แล้วส่งขึ้นบอร์ดดูผลทุกจุด อย่าเติมรวดเดียวหกจุดโปรแกรมนี้เป็นลูปไม่รู้จบ หยุดด้วย Ctrl+C ที่คอนโซล หรือกด RESTART บนหน้า Playground
เขย่าตอนหยุดบันทึกแล้วกราฟยังวิ่ง แปลว่า flag ยังไม่ได้ถูกใช้จริงในลูป
กราฟ accel 3 แกนวิ่งสด + ตารางสรุปค่าสุดขีด + ปุ่มเริ่มกับปุ่มหยุดที่แยกกัน
แปลเป็นสิ่งที่ตรวจได้จริง:
ข้อที่เจ็ดคือหัวใจ — ปุ่มที่กดแล้วไฟเปลี่ยนแต่ข้อมูลยังวิ่ง คือปุ่มที่ยังไม่ทำงาน
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
TypeError ตอนเรียก set_next |
ส่งทศนิยมเข้าไปตรง ๆ | ครอบด้วย int() ทุกครั้ง |
NameError: name 's_az' |
ยังไม่ได้เติมช่อง add_series แต่โค้ดข้างล่างอ้างถึงแล้ว |
เติมไล่จากบนลงล่าง |
| มีสี่เส้นแทนที่จะเป็นสาม | เผลอ add_series ให้แกน X ด้วย ทั้งที่ series 0 มีมาแล้ว |
ใช้ set_next(0, ...) กับแกน X |
| กราฟเดินอืด ๆ ทั้งที่ลูปตั้ง 200 ms | ลูปมีแต่ set_next() ซึ่งไม่ปลุกโหมดเร่ง — CM55 ระบายแค่ 80 คำสั่ง/วินาที |
เพิ่ม lbl.text(...) ในลูปเดียวกัน |
| กราฟกระตุก จุดหายเป็นช่วง | ลูปเร็วเกิน คำสั่งล้นคิว IPC 64 ช่องแล้วถูกทิ้ง | เพิ่ม sleep_ms กลับไปที่ 200 |
| เส้นแบนติดขอบบนตลอด | ช่วงแกน Y แคบไป แกน Z ชนเพดาน | ขยายเป็น min=-20, max=20 |
| กดหยุดบันทึกแล้วกราฟยังวิ่ง | if running: ไม่ได้ครอบส่วนที่ป้อนข้อมูล |
ย้าย set_next เข้าไปใต้ if running: |
| กดหยุดบันทึกแล้ว widget หายทั้งจอ | ไปหยุดลูป ทำให้ ui.poll() ไม่ถูกเรียก |
การหยุดคือ flag ห้ามหยุดลูป |
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| ตารางขึ้นแค่สองแถวครึ่ง แถว Z หายไป | คอลัมน์แคบกว่าข้อความ ข้อความตกบรรทัด ทุกแถวเลยสูงสองเท่า | ขยาย .col_width() จนไม่มีช่องไหนตกบรรทัด |
| ตัวหนังสือไทยในตารางขึ้นเป็นกล่องสี่เหลี่ยม | ช่องของตารางวาดจากคนละส่วนกับ Label | เฟิร์มแวร์ตั้งฟอนต์ไทยที่ส่วน ITEMS ให้แล้ว ถ้ายังเป็นกล่อง ให้แจ้งผู้สอน |
| ไฟดับแล้วยังเห็นเป็นวงจาง ๆ | ตั้งใจ .value(0) คือหรี่ ไม่ใช่หาย |
ไฟที่หายไปทำให้แยกไม่ออกว่าดับหรือจอเสีย |
| เส้นกราฟกระตุกทั้งที่ลูปยังเดิน | ทั้งลูปมีแต่ .value() กับ set_next() ซึ่งไม่ปลุกโหมดเร่ง |
ส่ง .text() อย่างน้อยหนึ่งครั้งต่อรอบ ส่งข้อความเดิมก็ได้ |
| กดปุ่มแล้วไม่มีอะไรเกิดขึ้น | เทียบ handle กับ id ผิดตัว หรือลืมเก็บ btn.id() |
ตรวจว่าเก็บ id ไว้ก่อนเข้าลูป |
| ค่าเซนเซอร์ค้างตัวเลขเดิม | หลังใช้ ui.* auto-task หยุด แต่เราไม่ได้อ่านเองในลูป |
อ่าน motion() ทุกรอบ |
| เขย่าเร็วแล้วกราฟโชว์คลื่นช้าแปลก ๆ | aliasing — เขย่าเกิน 2.5 Hz ที่ 5 Hz รับไหว | ไม่ใช่บั๊ก บันทึกไว้ |
สิบสองในสิบห้าข้อนี้ไม่มี error message — มีแต่ "กราฟดูแปลก ๆ" หรือ "ตารางดูแปลก ๆ" ต้องอ่านอาการเป็น
เปิด practise_codes/s07_accel_chart.py↗ มีช่องว่างให้เติม 6 จุด
# เติม: s_az = chart.add_series(COL_AZ)
pass
if running:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# เติม: chart.set_next(0, int(ax))
pass
# เติม: dt = time.ticks_diff(now, last_ms)
pass
for ev in ui.poll():
h = ev.get('handle')
if h == start_id:
# เติม: running = True
pass
elif h == stop_id:
# เติม: running = False
pass
# เติม: time.sleep_ms(PERIOD_MS)
pass
ลำดับการเติมสำคัญ — โค้ดข้างล่างอ้างถึงตัวแปรที่เกิดจากช่องว่างข้างบน
ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 20 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · สุ่มช้าไปแล้วได้ความถี่ผี · 8 นาที | examples/s07/03_aliasing_nyquist.py↗ |
ตั้งค่า PERIOD_MS ของทีมโดยมีเหตุผลรองรับ แทนที่จะเดาแล้วมาแก้ทีหลัง · จะเข้าใจว่าสุ่มช้าเกินไป แล้วได้ความถี่ที่ไม่เคยมีอยู่จริงบนกราฟ |
| 2 · เฝ้าการสั่นด้วยเกณฑ์ที่วัดเอง · 12 นาที | examples/s07/01_imu_vibration_monitor.py↗ |
ลอกโครงลูปอ่านเซนเซอร์ เส้นเกณฑ์ และปุ่มหยุดกราฟ ไปใส่ไฟล์ของทีมได้ทันที · จะได้รูปร่างของงานเฝ้าการสั่น คือกราฟคู่กับเส้นเกณฑ์ที่วัดมาเอง ไม่ใช่ตัวเลขที่หยิบมาจากที่อื่น |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| กดปุ่มเริ่ม/หยุดแล้วจอไม่เปลี่ยนอะไรเลย | examples/s04/02_event_types.py↗ — Button ส่ง clicked ส่วน Switch ส่ง toggled คนละชนิดกัน ดักผิดชนิดคือเงียบ |
อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของคาบ 7 เพราะ MVP วันนี้วัดที่กราฟสามเส้น ตาราง และปุ่มเริ่ม/หยุดบันทึก ไม่ได้วัดสเปกตรัม: examples/s07/02_fft64_two_tones.py↗ เขียน FFT 64 จุดด้วยมือทั้งตัว แล้วชี้ว่าข้อมูลชุดเดิมมองคนละมุมได้คำตอบคนละแบบ · พอเข้าใจข้างในแล้ว ของจริงใช้ dsp.fft_mag() ที่คำนวณใน C (firmware 2026-08-20 ขึ้นไป) — examples/lvgl_ports/sec3_sensor_viz/eva/ex16_spectrum_analyzer.py คือเครื่องวิเคราะห์สเปกตรัมทั้งเครื่องที่ประกอบจากมัน เก็บไว้เปิดตอนอยากต่อยอดไปงาน predictive maintenance จริง

examples/s07/02_fft64_two_tones.py↗ — บนคือคลื่นที่ป้อนเข้าไป ล่างซ้ายคือสเปกตรัมที่ออกมา วาดบนตารางไฟ 8x8 ด้วย DotMatrix.set_pixels() · ภาพจับที่ขั้นแรก ป้อนโทนเดียว 3 รอบเข้าไป ยอดจึงขึ้นที่ bin 3 เดี่ยว ๆ และเพราะความละเอียดต่อ bin เท่ากับ fs/N = 1.000 Hz เลข bin กับเลขความถี่จึงบังเอิญตรงกันพอดีที่ 3 · บรรทัดล่างคือราคาที่ประหยัดได้จริง FFT เร็วกว่า DFT ตรง ๆ 21 เท่าที่ N = 64 · ภาพหน้าจอจริงจากบอร์ด Eva Kit บันทึกโดยผู้สอนคาบนี้เหลือตัวอย่างสามไฟล์ หลังตัดเรื่องเลข int16 · บิตมาสก์ · การแพ็กฟิลด์ลงไบต์ ออกจากหลักสูตร ทั้งสามเรื่องไม่มีคาบไหนสอนแล้ว และไม่มีไฟล์ไหนต้องแก้ค่าคงที่ก่อนกด Run
s07_accel_chart.py↗ — ตั้งต้น กราฟ ตาราง ไฟ และปุ่มสองปุ่มimport ui
ui.screen()
import time
import sensors
COL_TEXT, COL_DIM = 0xE8EAED, 0x9AA3AF
COL_AX = 0x4A9EFF # แกน X - เส้นที่ 1 ของจานสีเส้นข้อมูล สีฟ้า
COL_AY = 0x8E7BFF # แกน Y - เส้นที่ 2 ของจานสีเส้นข้อมูล สีม่วง
COL_AZ = 0x2FB6A8 # แกน Z - เส้นที่ 3 ของจานสีเส้นข้อมูล สีเขียวน้ำทะเล
COL_RUN = 0x4A9EFF # สีเน้นของจานบทบาท - ใช้กับไฟบอกว่ากำลังบันทึกอยู่
PERIOD_MS = 200 # คาบการสุ่มที่เราเลือก
...
chart = ui.Chart(x=24, y=52, w=292, h=144, min=-20, max=20, color=COL_AX)
s_ay = chart.add_series(COL_AY)
s_az = chart.add_series(COL_AZ)
tbl = ui.Table(x=332, y=52, w=436, h=280, cols=4, value=16)
... # col_width + add_row สี่แถว - ท่าที่ 2
led_rec = ui.Led(x=24, y=212, w=48, h=48, color=COL_RUN, value=1)
lbl_rec = ui.Label("กำลังบันทึก", x=88, y=216, color=COL_TEXT, value=20)
lbl_dt = ui.Label("คาบลูป -- ms", x=88, y=252, color=COL_DIM, value=20)
... # ปุ่มเริ่ม/หยุด + start_id stop_id - ท่าที่ 3
lo = [None, None, None]
hi = [None, None, None]
- ไว้ก่อน ไม่ใช่เว้นว่าง เพราะช่องว่างเปล่าอ่านได้สองแบบ คือ "ยังไม่มีค่า" กับ "จอเสีย" ส่วนขีดอ่านได้แบบเดียว · lo กับ hi เกิดนอกลูปเพราะมันคือความจำของโปรแกรม · เส้นในกราฟใช้สามสีแยกแกน ส่วนตารางใช้ ตัวอักษร X Y Z แยกแทน เพื่อให้อ่านออกแม้พิมพ์เป็นขาวดำ · start_id กับ stop_id เก็บครั้งเดียวตอนสร้าง เพราะบนจอมีสองปุ่มrunning = True
last_ms = time.ticks_ms()
last_sec = -1
while True:
if running:
try:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
except OSError:
pass
chart.set_next(0, int(ax))
chart.set_next(s_ay, int(ay))
chart.set_next(s_az, int(az))
note(0, ax) # กราฟลืมจุดที่เก่ากว่า 50 จุด ตารางไม่ลืม
now = time.ticks_ms()
dt = time.ticks_diff(now, last_ms)
last_ms = now
for ev in ui.poll(): # poll นอก if running เสมอ ไม่งั้นปุ่มตายตอนหยุด
h = ev.get('handle')
if h == start_id:
running = True
lo = [None, None, None]
hi = [None, None, None]
led_rec.value(1)
elif h == stop_id:
running = False
led_rec.value(0) # หรี่ ไม่ใช่หาย
sec = time.ticks_ms() // 1000
if sec != last_sec: # ตัวเลขขยับวินาทีละครั้ง กราฟขยับ 5 ครั้ง
last_sec = sec
for i, v in enumerate((ax, ay, az)):
tbl.cell(i + 1, 1, cell(lo[i]))
lbl_dt.text("คาบลูป {} ms".format(dt))
lbl_rec.text(rec_msg) # ชีพจร - ส่งข้อความเดิมซ้ำทุกรอบโดยตั้งใจ
time.sleep_ms(PERIOD_MS)
ui.poll()อยู่ นอกif running:— ปุ่มหยุดที่ฆ่าทางกลับของตัวเองคือบั๊กที่เจอบ่อยที่สุดในหน้าที่มีปุ่มหยุด และมันดูเหมือนจอค้าง ทั้งที่โปรแกรมยังวิ่งครบทุกบรรทัด
วันนี้เราได้: เข้าใจว่า time series คืออะไรและอัตราสุ่มมีผลอย่างไร · รู้จัก Nyquist และ aliasing พร้อมตัวเลขของลูปเราเอง · ใช้ ui.Chart กับหลาย series ได้ · รู้ว่าทำไมต้อง int() และมันแลกอะไรไป · เลือกช่วงแกน Y ด้วยเหตุผลของตัวเอง · ทำปุ่มเริ่ม/หยุดบันทึกด้วย flag ที่ไม่ฆ่าลูป · วัดคาบลูปจริงและเอาขึ้นจอ · และรู้ว่าทำไมลูปที่มีแต่กราฟถึงช้ากว่าลูปที่มีป้ายด้วย 40 เท่า
รากฐานที่แตะไป: การสุ่มตัวอย่างและคาบการสุ่ม · ทฤษฎีบทไนควิสต์และ aliasing · การสูญเสียความละเอียดจากการแปลงเป็นจำนวนเต็ม · ring buffer และคิวที่มีขนาดจำกัด · การวัดเวลาด้วยตัวนับที่วนกลับ · สถานะที่ต้องอยู่นอกลูป · การใช้สีเป็นภาษาสื่อความหมาย · การทำให้โปรแกรมรายงานสมรรถนะของตัวเอง
คำถามคิดต่อ: ถ้าต้องส่งข้อมูลนี้ขึ้นคลาวด์ในคาบ 10 จะส่งทุกจุดที่ 5 Hz หรือส่งเฉพาะตอนที่มีอะไรน่าสนใจ · ค่าอะไรควรสรุปก่อนส่ง · ถ้าเน็ตหลุดสามนาที ข้อมูลช่วงนั้นควรหายไปเลยหรือควรเก็บไว้
เก็บไฟล์ของวันนี้ไว้ให้ดี คาบ 8 เราจะเปิดมันขึ้นมาต่อยอด ไม่ได้เริ่มใหม่
ทั้งสี่มุมใช้แนวคิดเดียวกับวันนี้ทั้งหมด ต่างกันที่ อัตราสุ่ม ความยาวหน้าต่างเวลา และสิ่งที่ทำต่อกับข้อมูล
ข้อ 1 · เครื่องกำเนิดสัญญาณของทีม
ต่อยอดจากโค้ดอุ่นเครื่อง ทำหน้าที่ป้อนคลื่นสี่แบบ (สี่เหลี่ยม ไซน์ สามเหลี่ยม ฟันเลื่อย) ลงกราฟโดยไม่ใช้เซนเซอร์เลย แล้วใส่ปุ่มสลับรูปคลื่น บันทึกว่าคลื่นแบบไหนดูผิดเพี้ยนเร็วที่สุดเมื่อเพิ่มความถี่ และเทียบกับที่ Nyquist ทำนายไว้
ข้อ 2 · กราฟความละเอียดสูง
เปลี่ยนไปใช้ int(ax * 100) พร้อมขยายช่วงเป็น min=-2000, max=2000 แล้ววางเทียบกับกราฟเดิม บันทึกว่าเห็นรายละเอียดอะไรเพิ่มขึ้น และเสียอะไรไปหรือไม่
ข้อ 3 · ตารางทดลอง cadence
ทดลอง PERIOD_MS ที่ 500, 200, 100, 50 และ 20 บันทึกคาบลูปจริงที่วัดได้ในแต่ละค่า แล้วหาว่าจุดไหนที่เริ่มเห็นอาการเฟรมหาย · โจทย์เพิ่ม: ทำการทดลองซ้ำสองรอบ รอบหนึ่งมีบรรทัด lbl.text() รอบหนึ่งไม่มี แล้วเทียบตัวเลข
ข้อ 4 · เส้นดิบเทียบเส้นกรอง
เพิ่ม series ที่สี่เป็นค่าที่ผ่าน dsp.EMA(alpha=0.2) ของแกนใดแกนหนึ่ง วางทับเส้นดิบของแกนเดียวกัน แล้วอธิบายว่าฟิลเตอร์แลกอะไรกับอะไร (ลองปรับ alpha เป็น 0.05 กับ 0.5 ประกอบ)
เขียนคำตอบลง worksheet ข้อ 8 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า

แหล่งปฐมภูมิ
.../ipc_ui/ipc_ui.c — IPC_UI_TIMER_MS, IPC_UI_FAST_TIMER_MS, IPC_UI_MAX_PER_TICK, IPC_UI_FAST_TIMEOUT_MS, ui_arm_fast_mode() (สไลด์ผู้สอน)วิดีโอที่ฝังไว้ในคาบนี้ (ตรวจแล้วว่าเปิดได้ ตาม _BUILD/MEDIA_PLAN.md)
nac9qyJT-sY An intuitive introduction to oversampling and noise shaping — Analog Snippets · 12:08 · plq_Nmud5CM ADC Quantization and Resolution — Microchip Developer Help · ยาวยังไม่ยืนยัน · yYHHuhDwhec Sigma-Delta ADC: Oversampling, Noise Shaping & Decimation — Byte-Sized Learning · 5:01 · RLQGZl0lpjQ Working principle of an accelerometer — Bosch Sensortec · 1:01 — คำอธิบายว่าดูเพื่ออะไร อยู่ใต้คลิปในสไลด์ที่ฝังไว้แล้ว
เครดิตภาพ — Wikimedia Commons และ PMC open-access (CC0 / CC BY / CC BY-SA / PD) · รายละเอียดที่ CREDITS.md
fit-css
VIDEO-SLOT: คลิป 45-60 วินาที ถ่ายจอบอร์ดที่เมนู Sensor Dashboard — เริ่มจากบอร์ดวางนิ่งให้เห็นเส้นสามสีเกือบเรียบ แล้วเขย่าเบา ๆ ให้เห็นเส้นกระเพื่อม แล้วเอียงบอร์ดค้างไว้ให้เห็นเส้นหนึ่งยกตัวค้างแล้วเลื่อนไปทางซ้าย
.set_next() ไม่เคยฟ้อง และ .value() ของ Chart ก็ไม่เคยตอบ — กราฟจึงเป็น widget ที่จอเป็นพยานคนเดียว ถ้าไม่มีป้ายตัวเลขคู่กันไว้ ก็ไม่มีทางรู้ว่าข้อมูลเข้าไปจริงไหม
VIDEO-SLOT: คลิป 30-45 วินาที ถ่ายผลลัพธ์ MVP — เริ่มที่บอร์ดวางนิ่งให้เห็นสามเส้นแยกสี เส้น Z ลอยสูง แล้วเขย่าให้เห็นทั้งสามเส้นตอบสนองและรอยเลื่อนไปทางซ้าย พร้อมตัวเลขสูงสุดในตารางที่ขยับตาม จากนั้นกด "หยุดบันทึก" ให้เห็นไฟหรี่ลงและป้ายเปลี่ยนเป็น "หยุดแล้ว" แล้วเขย่าแรง ๆ ให้เห็นว่ากราฟนิ่งสนิทแต่ตารางยังค้างค่าสุดขีดของรอบที่แล้วไว้ ปิดท้ายด้วยการกด "เริ่มบันทึก" แล้วซูมให้เห็นค่าสุดขีดถูกล้างกลับเป็นขีด