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 |
ทำไมไม่ใช้ แดง/เขียว/น้ำเงิน — จานสีชุดนี้ไม่มีสีสถานะปนอยู่เลยสักตัว โดยตั้งใจ หน้าจอนี้มีไฟเตือนอยู่ด้วย และ เกณฑ์หน้าจอของหลักสูตร ห้ามใช้สีของการแจ้งเตือนกับอย่างอื่นในหน้าจอเดียวกัน เส้น 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 รู้เรื่องการวนนี้และคืนค่าที่ถูกต้องเสมอ
โปรแกรมที่วัดตัวเองได้ ดีบักง่ายกว่าโปรแกรมที่ต้องเดาเสมอ
บทเรียน 2.4–2.6 เราเจอสี่เมธอดนี้กับกล่องสี่เหลี่ยมใบเดียว วันนี้ขอย้ำอีกครั้ง เพราะ 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() วิธีแก้เดียวกัน: มีป้ายสถานะ "ตอนนี้อยู่หน้าไหน" อัปเดตไปด้วย · ของจริง: 07_find_move_hide_delete.py นับ ui.list() ให้ดูว่าซ่อนแล้วตัวเลขไม่ลด แต่ลบแล้วลดย้ายของที่มีอยู่แล้วถูกกว่าสร้างใหม่เสมอ — ทั้งเรื่องงบ 64 ช่อง และเรื่องเวลาที่ CM55 ต้องใช้วาด
ui — ตารางอ้างอิง กับกับดักประจำตัวของแต่ละตัวบทเรียน 1.1–3.3 เราใช้ไปแล้วห้าหกตัว วันนี้เพิ่ม 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() มีทั้งสองบอร์ด เพราะมีชิปเสียงเหมือนกัน — 05_door_open_switch.py ใช้ ui.tone() อยู่จริง
ตัวไหนมีของให้ลองอยู่ที่ไหน — DotMatrix + .set_pixels() ที่ 02_fft64_two_tones.py (สเปกตรัมเป็นตารางไฟ) และ 14_the_board_hears_you.py · Image .icon() และ Spinner ที่ 15_one_number_many_faces.py · Dropdown กับ Textarea ที่ 08_dropdown_textarea.py · เหลือ .set_image() ตัวเดียวที่ไม่มีไฟล์ไหนใช้ ด้วยเหตุผลย่อหน้าบน
รู้ว่ามีอะไรอยู่ในกล่องเครื่องมือ สำคัญกว่าท่องวิธีใช้ทุกตัว — เปิดตารางนี้ตอนคิดไม่ออกว่าจะแสดงค่านี้ด้วยอะไรดี
สไลด์ "ย้าย ย่อ ซ่อน" หยิบมาสี่ตัว สไลด์นี้กางสิบสี่ชื่อที่กราฟกับการจัดหน้าต้องใช้ — บัญชีเต็มของเฟิร์มแวร์มีมากกว่านี้ (เช่น .cell() .add_row() .col_width() .prop() .ticks() .digits() ที่ตาราง ไม้บรรทัด และช่องตัวเลขใช้) ดูบัญชีทั้งโมดูลในบทเรียน 2.4
เก้าตัวที่ใช้ได้กับ widget ทุกชนิด รวมทั้ง Chart เพราะทุกชนิดใช้ตารางเมธอดชุดเดียวกัน
| เมธอด | ทำอะไร | ชุดบทเรียนนี้ใช้ตรงไหน |
|---|---|---|
.id() |
เลขประจำตัว 0-63 ที่ตรงกับ ev['handle'] |
ใช้ — แยกว่าปุ่มเริ่มหรือปุ่มหยุดถูกกด |
.text() / .text("...") |
อ่านคืน None เสมอ / เขียนข้อความทับ |
ใช้ทุกรอบ — ป้ายค่าและป้ายสถานะ · และเป็นตัวที่ทำให้กราฟลื่นขึ้นตามสไลด์ "ทำไมกราฟช้า" |
.value() / .value(n) |
อ่าน / สั่งค่า มีสิบเอ็ดชนิดที่ตอบได้จริง (บัญชีอยู่ในบทเรียน 2.5) | ไม่ใช้ในชุดบทเรียนนี้ · 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 — ตัวหลังโผล่จริงในตัวอย่างเสริม 02_fft64_two_tones.py
ฝั่งตัวสร้างที่ชุดบทเรียนนี้กางให้ดูมีสิบหกตัว ตารางอ้างอิงข้างบนกางไว้สิบชนิด อีกหกตัวคือของที่เราใช้มาตั้งแต่บทเรียน 1.1–3.3 อยู่แล้ว: Label Button Switch Slider Checkbox Bar — บัญชีเต็มของทั้งโมดูล ui อยู่ในบทเรียน 2.4–2.6

กราฟเวลาที่เราทำวันนี้ตอบได้ว่า "ค่าเปลี่ยนไปยังไงตามเวลา" แต่ตอบไม่ได้ว่า "การสั่นนี้ประกอบด้วยความถี่อะไรบ้าง"
ภาพขวาคือข้อมูลจริงจากงานเฝ้าระวังตลับลูกปืน แถวบนเป็นโดเมนเวลา แถวล่างเป็นสเปกตรัมของข้อมูล ชุดเดียวกันเป๊ะ — กราฟเวลาของตัวปกติกับตัวเสียดูคล้ายกันมาก แต่สเปกตรัมแยกออกทันที
นี่คือสาเหตุที่งาน predictive maintenance ในโรงงานทำ FFT ก่อนตัดสินใจ ไม่ได้ดูกราฟเวลาอย่างเดียว สเปกตรัมคือที่ที่ความต่างซ่อนอยู่ ทั้งที่กราฟเวลาบอกไม่ได้
เชื่อมกับวันนี้: ที่ Hz เราทำ FFT ที่มีประโยชน์ไม่ได้ เพราะเห็นได้แค่ถึง 2.5 Hz ส่วนความสั่นของเครื่องจักรจริงอยู่หลักกิโลเฮิรตซ์ — อัตราแบบนั้นลูป Python เอื้อมไม่ถึง ต้องให้ เฟิร์มแวร์เป็นคนสุ่มตัวอย่าง แล้วส่งค่าที่ยุบแล้วขึ้นมาให้ เหมือนที่โมดูล mic ทำในบทเรียน 3.7–3.9
โมดูล 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' — บทเรียน 2.4–2.6)
ปุ่มเริ่มกับปุ่มหยุดแยกกันคนละใบ ไม่ใช่ปุ่มเดียวสลับ ปุ่มที่เขียนว่า 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 ของตัวเองครั้งเดียว) ค่าจึงสอดคล้องกันและถามแค่รอบเดียว — เหตุผลเดียวกับบทเรียน 3.1–3.3 · เราส่ง 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 ให้สงสัยว่าอัปเดตป้ายมากเกินจำเป็น ลองลดจำนวนป้ายที่อัปเดตทุกรอบลงแล้ววัดใหม่ — แต่ อย่าลดจนเหลือศูนย์ ไม่งั้นโหมดเร่งจะหลุด แล้วกราฟจะกระตุกแทน
ตัวเลขนี้คือ หลักฐาน ไม่ใช่ของประดับ — เอาไว้ตอบคำถามว่าลูปเรายังทันไหม
เห็นเส้นทางนี้แล้ว จะรู้ว่าเวลากราฟผิดปกติ ควรไปดูที่ จังหวะการส่ง ก่อนไปโทษเซนเซอร์
s07_accel_chart.pypass ไล่จากบนลงล่างทีละจุด แล้วส่งขึ้นบอร์ดดูผลทุกจุด อย่าเติมรวดเดียวหกจุดNone ช่องขึ้น -) แล้วเริ่มเก็บใหม่จากค่าแรกของรอบนี้โปรแกรมนี้เป็นลูปไม่รู้จบ หยุดด้วย Ctrl+C ที่คอนโซล หรือกด RESTART บนหน้า Playground
เขย่าตอนหยุดบันทึกแล้วกราฟยังวิ่ง แปลว่า flag ยังไม่ได้ถูกใช้จริงในลูป
.set_next() ไม่เคยฟ้อง และ .value() ของ Chart ก็ไม่เคยตอบ — กราฟจึงเป็น widget ที่จอเป็นพยานคนเดียว ถ้าไม่มีป้ายตัวเลขคู่กันไว้ ก็ไม่มีทางรู้ว่าข้อมูลเข้าไปจริงไหม