จบชุดบทเรียนนี้เราจะทำครบ 4 เรื่อง แล้วปิดท้ายด้วย remix ที่รันได้จริง:
01 IMU, 02 tilt, 04 radar) แล้วเห็นว่ามันต่างกันแต่โครงเหมือนกันui.poll (+ ปุ่ม back) โครงที่เจอซ้ำทั้งคอร์สsensors.* — อ่านค่าจริงจากบอร์ด และ dsp.tilt — ยกงานคณิตให้ฝั่ง C ทำปลายทางของวันนี้: s02_anatomy_sensor.py — remix ที่ผสมทั้งสามตัวอย่าง เอียงบอร์ดแล้วเข็มขยับ มุมเด่นขึ้นจอ ป้ายเปลี่ยนสีเมื่อเอียงเกินเกณฑ์
วันนี้ยังไม่เขียนของใหม่จากศูนย์ เราฝึก "อ่านออก + แก้เป็น" ก่อน — พอจับโครงร่วมได้ ชุดบทเรียนถัด ๆ ไปการสร้างเองจะง่ายขึ้นมาก
ชุดบทเรียนก่อนหน้าเราเปิดคอร์สด้วยการรันเมนู 6 โมเดล อ่านคลาสที่ชนะ + ความมั่นใจได้ — นั่นคือขั้น Apps (ปลายวงจร)
ui.pollเราเลือกแกะ "แอปเซนเซอร์" ก่อนแอป AI เพราะมันมีแค่ อ่านค่า → คำนวณ → วาดจอ ไม่มีเรื่องโมเดล/IPC ข้ามคอร์มาบัง ทำให้เห็นโครงเปล่าๆ ชัดที่สุด
เปิด 01_imu_6axis.py รันดูก่อน แล้วขยับ/เขย่าบอร์ด ตัวเลข 6 แกนวิ่งตามทันที:
import ui
ui.screen()
import lcd, sensors, time
ui.Label("BMI270 - Accelerometer + Gyroscope", x=210, y=10, color=0xFFFFFF)
la = [ui.Label("AX: --", x=60, y=60 + i * 40, color=0x44CCFF) for i in range(3)]
mag_bar = ui.Bar(x=60, y=225, w=650, min=0, max=300, value=98)
back = ui.Button("< ออก", x=640, y=350, w=130, h=38); back_id = back.id()
while True:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion() # อ่าน 6 แกนพร้อมกัน
mag = (ax*ax + ay*ay + az*az) ** 0.5
mag_bar.value(int(mag * 10)) # วางนิ่ง ~98
for ev in ui.poll():
if ev.get('handle') == back_id:
raise KeyboardInterrupt
time.sleep_ms(100)
ก่อนจะแกะ ให้รันมันก่อนเสมอ — เห็นของทำงานแล้วสมองจะอยากรู้ "มันทำงานยังไง" นี่คือหัวใจของ กลับด้าน
เปิด 04_radar_presence.py แล้วนั่งนิ่งหน้าบอร์ด vs ออกนอกระยะ ป้ายเปลี่ยนจาก CLEAR เป็น PRESENCE:
panel = ui.Panel(x=100, y=50, w=590, h=220, color=0x115511)
state = ui.Label("CLEAR", x=330, y=140, color=0xFFFFFF)
back = ui.Button("< ออก", x=640, y=350, w=130, h=38); back_id = back.id()
was = None
while True:
r = sensors.radar()
now = bool(r["presence"])
if now != was: # วาดใหม่ "เฉพาะตอนสถานะเปลี่ยน"
panel.color(0x881111 if now else 0x115511)
state.text("PRESENCE" if now else "CLEAR")
was = now
for ev in ui.poll():
if ev.get('handle') == back_id:
raise KeyboardInterrupt
time.sleep_ms(300)
สังเกตว่าโค้ดคนละเซนเซอร์ (
radar()ไม่ใช่bmi270.motion()) คนละหน้าตาจอ แต่วางตัวเหมือนกันเป๊ะ — นี่แหละคือ "โครงร่วม" ที่เราจะแกะกัน
01 โชว์ตัวเลข 6 แกน · 02 โชว์เกจมุมเอียง · 04 โชว์ป้ายมีคน/ไม่มีคน — เซนเซอร์คนละตัว จอคนละแบบ แต่ถ้ามองข้ามรายละเอียด ทั้งสามเดินตาม สี่จังหวะ เดียวกัน
พอเห็นโครงร่วม การอ่านโค้ดใหม่ๆ จะเร็วขึ้นมาก เพราะคุณรู้ว่า "จะหาอะไรตรงไหน" — ที่เหลือคือรายละเอียดของแต่ละแอป
จำภาพนี้ให้ขึ้นใจ โปรแกรม MicroPython บน BENTO เกือบทุกตัวเดินตามนี้ (เราเห็นผ่านๆ มาแล้วในบทเรียน 1.1–1.3 วันนี้แกะเต็ม):
เราจะไล่ทีละจังหวะในสไลด์ถัดไป จำไว้ว่าจังหวะ 1–2 คือ "ตั้งเวที" ทำหนเดียว ส่วน 3–4 คือ "การแสดง" ที่วนไม่หยุด
บรรทัดแรกๆ ของทุกไฟล์คือการบอกว่าเราจะใช้โมดูลอะไรบ้าง แต่ละตัวเปิดประตูสู่คนละความสามารถ:
import ui
ui.screen() # เคลียร์จอ + เตรียม canvas 792x398 (เรียกครั้งเดียว ทันทีหลัง import ui)
import lcd # console ข้อความด้านล่างจอ (lcd.console / lcd.clear)
import sensors # อ่านเซนเซอร์จริงบนบอร์ด (bmi270 / radar / ...)
import dsp # ยกงานคณิต/ฟิสิกส์ให้ฝั่ง C (tilt / altitude / ...)
import time # จังหวะเวลา (time.sleep_ms)
ui.screen() เรียก ทันทีหลัง import ui เสมอ เพื่อเคลียร์จอและจองพื้นที่วาด — ถ้าลืม widget อาจไปวางทับหน้าเก่า01/04 ไม่ได้ import dsp เพราะไม่ต้องคำนวณมุม ส่วน 02 และ remix ของเราต้องใช้ จึง import เพิ่มimport คือการประกาศ "ฉันจะใช้อะไรบ้าง" — อ่าน import ให้จบก่อน แล้วคุณจะเดาได้เลยว่าแอปนี้แตะเซนเซอร์ตัวไหน วาดจอด้วยอะไร
ก่อนเข้าลูป เราสร้าง widget ทุกตัวที่จะใช้ให้ครบ แล้วเก็บ "ตัวอ้างอิง" ไว้ในตัวแปร เพื่อจะไปแก้ค่าทีหลัง:
ui.Label("Pitch", x=170, y=24, color=CYAN) # ป้ายนิ่งๆ ไม่ต้องเก็บตัวแปร
arc_p = ui.Arc(x=100, y=54, min=-90, max=90, value=0) # เก็บ arc_p ไว้แก้ค่าในลูป
seg = ui.Seg7("0", x=330, y=104, color=GREEN) # เก็บ seg ไว้เปลี่ยนตัวเลข
back = ui.Button("< ออก", x=640, y=350, w=130, h=38)
back_id = back.id() # จำ id ปุ่มไว้เทียบตอน poll
arc_p, seg) · ป้ายที่วางแล้วนิ่งไม่ต้องเก็บback.id() คืนเลขประจำปุ่ม เราเก็บเป็น back_id ไว้เทียบกับอีเวนต์ที่ ui.poll() ส่งกลับมาคิดภาพ widget เป็นป้ายที่ตอกติดผนังไว้แล้ว ในลูปเราแค่เดินไปเปลี่ยนตัวเลขบนป้าย — เราไม่รื้อป้ายเก่าแล้วตอกใหม่ทุกวินาที
นี่เป็นกฎที่พลาดบ่อยและเจ็บจริง ถ้าเผลอสร้าง widget ในลูป จะเกิดสองปัญหาพร้อมกัน:
กฎง่ายๆ: สิ่งที่ "เป็น" อยู่ สร้างก่อนลูป · สิ่งที่ "เปลี่ยน" ทำในลูป — จำประโยคนี้ไว้ จะไม่พลาดเรื่อง widget อีกเลย
หัวใจของทุกแอปอยู่ในลูป และในลูปมีจังหวะย่อยสามท่อนเสมอ อ่านเซนเซอร์ → คำนวณ → วาดจอ:
while True:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion() # (ก) อ่าน: ดึงค่าจากเซนเซอร์
roll, pitch = dsp.tilt(ax, ay, az) # (ข) คำนวณ: แปลงเป็นมุม
p = 0 if abs(pitch) < DEAD else int(pitch) # จัดค่าให้พร้อมโชว์
if (p, r) != last: # (ค) วาดเฉพาะตอนค่าเปลี่ยน
arc_p.value(p)
seg.text("%d" % ang)
last = (p, r)
...
time.sleep_ms(100) # เว้นจังหวะ ไม่รัดจอจนกิน CPU
time.sleep_ms(100) = ลูปวิ่ง ~10 ครั้ง/วินาที เร็วพอให้ลื่น แต่ไม่รัดจน CPU/IPC ทำงานหนักเปล่าเห็นโครง "อ่าน–คำนวณ–วาด" นี้ไหม? ทั้ง
01/02/04และ remix ของเราใช้อันเดียวกันหมด ต่างแค่ "อ่านอะไร คำนวณอะไร วาดด้วย widget อะไร"
จอ BENTO เป็นทัชสกรีน ทุกการกดปุ่ม/แตะจอจะถูกเก็บเป็น "อีเวนต์" เราดึงมาอ่านด้วย ui.poll() หนึ่งครั้งต่อรอบลูป:
for ev in ui.poll(): # ดึงอีเวนต์ที่ค้างในคิวออกมาทีละตัว
if ev.get('handle') == back_id: # ใช่ปุ่ม back ที่เราจำ id ไว้ไหม
raise KeyboardInterrupt
ui.poll()เป็นแบบ non-blocking — ถ้าไม่มีใครกด มันคืน list ว่างแล้วลูปวิ่งต่อ ไม่ค้างรอ นี่คือเหตุผลที่จอยังอัปเดตค่าเซนเซอร์ได้ลื่นๆ พร้อมกับฟังปุ่มไปด้วย
ทุกแอปในคอร์สมีปุ่ม back ที่ทำงานเหมือนกัน และมันผูกกับวิธี "ออกจากลูปอย่างเรียบร้อย":
try:
while True:
...
for ev in ui.poll():
if ev.get('handle') == back_id:
raise KeyboardInterrupt # กด back = โยนสัญญาณออกจากลูป
time.sleep_ms(100)
except KeyboardInterrupt:
lcd.console('<span class=ok> จบการทำงาน</span>') # เก็บกวาดตอนจบ
raise KeyboardInterrupt → กระโดดออกจาก while ไปที่ except ทันทีexcept (หรือ finally) เราทำการ เก็บกวาด — พิมพ์ข้อความจบ หรือหยุดเครื่องยนต์ (แบบที่บทเรียน 1.1–1.3 เรียก edge_ai.stop())
raiseกลางลูปดูแรง แต่จริงๆ คือวิธี "ออกจากงานอย่างมีระเบียบ" — เราไม่ปล่อยให้โปรแกรมจบดื้อๆ แต่ให้มันวิ่งผ่านexceptเพื่อเก็บของให้เรียบร้อยก่อน
พอมีโครงในหัวแล้ว ลองวางสามแอปลงตารางเดียว จะเห็นว่าต่างกันแค่ช่อง "อ่านอะไร / คำนวณอะไร / วาดด้วยอะไร":
| จังหวะ | 01 IMU 6-axis |
02 Tilt |
04 Radar |
|---|---|---|---|
| 1 import | ui·sensors·time | + dsp |
ui·sensors·time |
| 2 สร้างครั้งเดียว | Label ×6 + Bar | Arc ×2 + Seg7 | Panel + Label |
| 3 อ่าน | bmi270.motion() |
bmi270.motion() |
sensors.radar() |
| 3 คำนวณ | ` | a | ` = √(ax²+ay²+az²) |
| 3 วาด | label.text + bar.value |
arc.value + seg.text |
panel.color + state.text |
| 4 poll + back | เหมือนกันทุกบรรทัด | เหมือนกันทุกบรรทัด | เหมือนกันทุกบรรทัด |
01, ช่อง "คำนวณ" จาก 02, ลูกเล่น "วาดเฉพาะตอนเปลี่ยน" จาก 04ตารางนี้คือแผนที่ remix ของคุณ — อยากเปลี่ยนแอปให้อ่านเซนเซอร์อื่น ก็แค่สลับช่อง "อ่าน" กับ "คำนวณ" ส่วนโครงที่เหลือไม่ต้องแตะ
sensors คือหน้าต่างเดียวที่เราอ่านค่าจริงจากฮาร์ดแวร์บนบอร์ด แต่ละเซนเซอร์มีฟังก์ชันของตัวเอง ชุดบทเรียนนี้เราใช้สองตัว:
| คำสั่ง | คืนค่า |
|---|---|
sensors.bmi270.motion() |
(ax, ay, az, gx, gy, gz) — accel 3 + gyro 3 อ่านพร้อมกัน |
sensors.bmi270.acceleration() |
(ax, ay, az) — เฉพาะ accel |
sensors.bmi270.chip_id() |
int — เลขรุ่นชิป (ใช้เช็กว่าต่อเซนเซอร์ติด) |
sensors.radar() |
{'presence', 'energy'} — มีคนไหม + พลังงานสะท้อน |
sensors.dps368.pressure_temperature() |
(hPa, °C) — ความดัน/อุณหภูมิ (ชุดบทเรียนหลัง ๆ) |
sensors.sht40.temperature_humidity() |
(°C, %RH) — อุณหภูมิ/ความชื้น (ชุดบทเรียนหลัง ๆ) |
bmi270.motion() (IMU) กับ radar() — สองตัวที่เราเพิ่งเห็นใน 01 และ 04
motion()อ่าน 6 แกน ในครั้งเดียวใต้ bus lock เดียว = ได้ค่าที่เวลาตรงกันทุกแกน ถ้าอ่านทีละแกนแยกกัน ค่าจะคนละเสี้ยววินาที เอามาคำนวณมุมจะเพี้ยน
IMU (Inertial Measurement Unit) BMI270 วัดสองอย่าง: ความเร่ง (accelerometer) กับ การหมุน (gyroscope) อย่างละ 3 แกน:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# ax, ay, az = ความเร่ง 3 แกน หน่วย m/s2 (วางนิ่งแกนที่ตั้งฉากพื้นจะได้ ~9.8 จากแรงโน้มถ่วง)
# gx, gy, gz = ความเร็วเชิงมุม 3 แกน หน่วย dps (องศา/วินาที) — บอกว่าบอร์ดกำลังหมุนเร็วแค่ไหน
dsp.tilt)az ≈ 9.8, ที่เหลือ ≈ 0 · เอียงบอร์ด: แรงโน้มถ่วงกระจายไป ax/ay ด้วยชุดบทเรียนนี้ remix ของเราใช้แค่ 3 แกน accel (ax, ay, az) เพื่อคำนวณมุมเอียง ส่วน gyro เราอ่านมาเก็บไว้เฉยๆ เผื่อคุณอยากต่อยอด (เช่น จับการหมุนเร็ว)
ใน 01 มีบรรทัด mag = (ax*ax + ay*ay + az*az) ** 0.5 นั่นคือการหา ขนาด (magnitude) ของเวกเตอร์ความเร่ง 3 แกน รวมเป็นตัวเลขเดียว:
อ่านทีละสัญลักษณ์แบบง่ายๆ:
bmi270.motion() คืนมา 3 ตัวแรก)ทำไมมันสำคัญกับชุดบทเรียนนี้: 01 เอา |a| ไปป้อน mag_bar — วางบอร์ดนิ่งแท่งจะอยู่ราว 9.8 (จาก ) พอเขย่าเลขจะพุ่งขึ้น นี่คือวิธี "ยุบ 3 แกนเป็นค่าเดียว" ที่ดูการเคลื่อนไหวรวมได้ในแท่งเดียว โดยไม่ต้องจ้องทั้งสามตัวเลขพร้อมกัน
สังเกตว่าโค้ดเขียน
** 0.5แทนsqrt— ยกกำลัง 0.5 คือรากที่สองพอดี (ทั้งคู่ให้ผลเท่ากัน) เป็นเหตุผลว่าทำไมคณิตนิดเดียวถึงแปลงเป็นโค้ดบรรทัดเดียวได้เลย
เรดาร์ 60 GHz ยิงคลื่นออกไปแล้ววัดคลื่นสะท้อน ตรวจได้ว่ามีสิ่งเคลื่อนไหวอยู่หน้าบอร์ดไหม โดย ไม่ต้องใช้กล้อง:
r = sensors.radar()
# r = {'presence': 1, 'energy': 1234.0}
now = bool(r["presence"]) # True = พบการเคลื่อนไหว
e = r["energy"] # พลังงานสะท้อน (มากขึ้นเมื่อวัตถุใกล้/ใหญ่)
'presence' — เฟิร์มแวร์ตัดสินให้แล้วว่า "มีคน/ไม่มี" (1 หรือ 0) เราเอาไป bool() ต่อได้เลย'energy' — ค่าดิบของพลังงานสะท้อน ใช้ดูแนวโน้ม (ใกล้เข้ามา energy สูงขึ้น)radar() คุยข้ามคอร์ไป CM55 เหมือน edge_ai — ถ้า timeout จะโยน OSError (ในงานจริงควรห่อ try)เรดาร์เด่นตรงที่ทำงานในที่มืด ผ่านวัสดุบางอย่างได้ และไม่บันทึกภาพ = เป็นส่วนตัวกว่ากล้อง เหมาะกับสวิตช์ไฟอัตโนมัติ ป้ายอัจฉริยะ ที่ไม่อยากให้มีกล้องจ้อง
จาก accel 3 แกนดิบ เราอยากได้ "บอร์ดเอียงกี่องศา" การคำนวณนี้ใช้ตรีโกณ (atan2) ซึ่งถ้าเขียนใน MicroPython จะช้า dsp จึงยกไปคำนวณฝั่ง C ให้:
import dsp
roll, pitch = dsp.tilt(ax, ay, az) # คืน (roll, pitch) หน่วยองศา
# roll = เอียงซ้าย-ขวา (หมุนรอบแกนหน้า-หลัง)
# pitch = เอียงหน้า-หลัง (ก้ม-เงย)
dsp.tilt รับ accel 3 แกน คืนมุมสองค่าเป็น องศา พร้อมใช้ ไม่ต้องแปลงเรเดียนเองmath.atan2) แต่ dsp เร็วกว่าเพราะรันบน C — บทเรียน Processing (บทเรียน 3.1–3.2+) เราจะแกะข้างในว่ามันทำอะไรdsp ยังมี dsp.altitude(p), dsp.compass(mx,my,mz), dsp.dew_point(t,rh) ฯลฯ ไว้ใช้ชุดบทเรียนหลัง ๆนี่คือแนวคิดสำคัญ: งานคณิตหนักๆ ยกให้ C ทำ MicroPython เก่งเรื่องเรียบเรียง logic + คุมจอ ส่วนเลขเยอะๆ ให้ฝั่ง C ที่เร็วกว่าจัดการ เราแค่เรียกใช้ผลลัพธ์
บทเรียนสำคัญจาก 04: อย่าวาดจอทุกเฟรม วาดเฉพาะตอนค่าเปลี่ยนจริง ทำให้จอนิ่ง สายตาไม่ล้า และ IPC ไม่รก
last = (999, 999) # ค่าที่วาดครั้งก่อน (ตั้งเป็นค่าที่เป็นไปไม่ได้)
while True:
...
if (p, r) != last: # เปลี่ยนจากครั้งก่อนไหม
arc_p.value(p); arc_r.value(r) # เปลี่ยน → วาดใหม่
last = (p, r) # จำค่าล่าสุดไว้เทียบรอบหน้า
# ไม่เปลี่ยน → ข้าม ไม่แตะจอเลย
last, was, last_seq) แล้วเทียบก่อนวาดlast_seq — เช็ก seq ก่อนวาดผลอนุมานใหม่DEAD = 2.0) ช่วยอีกชั้น: มุมสั่นเล็กๆ ใกล้ 0 ปัดเป็น 0 เข็มจะได้ไม่กระตุก"วาดเฉพาะตอนเปลี่ยน" เป็นนิสัยที่ดีของงาน embedded — จอที่กระพริบตลอดไม่ใช่แค่กวนตา แต่เปลืองพลังและทำให้ช่องสื่อสารข้ามคอร์ (IPC) แน่นโดยไม่จำเป็น
เราทำตามลำดับ PRIMM ที่เกริ่นในบทเรียน 1.1–1.3 แต่ชุดบทเรียนนี้เน้นสอง P กลาง (Investigate + Modify) เป็นพิเศษ:
อ่านโค้ดแล้วรันดู แล้วค่อยแก้ทีละบรรทัด — สามก้าวนี้คือวิธีที่วิศวกรจริงเรียนของใหม่ ไม่ใช่นั่งท่องไวยากรณ์ให้ครบก่อนแตะโค้ด