# บน Eva ไม่มี sensors.init() ให้เรียก - คอร์จอถือบัส IMU เรียกแล้วได้ OSError ทันที
# บน Dev Kit CM33 อ่าน IMU ตรงจาก I2C เอง และเฟิร์มแวร์ปลุกมันไว้ตั้งแต่บูต จึงไม่ต้อง init เช่นกัน
smooth = dsp.EMA(alpha=0.2) # กันค่ากระโดดครั้งเดียวจนเตือนผิด
last_value = 0.0
stale = False # รอบนี้อ่านไม่ได้ใช่ไหม - จอต้องบอกความจริงข้อนี้
def read_value():
global last_value, stale
try:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
except OSError:
stale = True # ค่าค้างยังมีประโยชน์ แต่ต้องไม่ถูกโชว์เหมือนค่าสด
return last_value
stale = False
roll, pitch = dsp.tilt(ax, ay, az) # dsp.tilt คืน (roll, pitch) - roll มาก่อน
# ทีมเขียนเอง: เปลี่ยนบรรทัดล่างเป็นปริมาณที่โจทย์ของทีมสนใจจริง ๆ
last_value = smooth.update(abs(roll))
return last_value
read_value() คืน ค่าเดียว ไม่ใช่หกแกน — สามช่องที่เหลือของ canvas ตัดสินจากมัน แสดงมัน และส่งมัน · ธง stale มีไว้เพื่อข้อเดียว: ค่าที่แสดงต้องบอกคุณภาพของตัวเองได้ อุปกรณ์ที่อ่านเซนเซอร์ไม่ได้แล้วโชว์เลขเดิมค้างไว้คือเครื่องที่โกหกคนหน้างาน — ท่าที่ 3 จะเอาธงนี้ขึ้นจอว่า "ค่าค้าง อ่านไม่ได้" · ทีมความสั่นเปลี่ยนสองบรรทัดสุดท้ายเป็นขนาดความเร่งรวม ทีมการเคลื่อนย้ายใช้ sensors.bmm350.heading() โครงที่เหลือไม่ต้องแตะ
ถ้าทีมยังตอบไม่ได้ว่า "ค่าเดียว" ของทีมคืออะไร แปลว่าช่อง Sense ในบันทึกการเรียนยังกรอกไม่เสร็จ
LED_NAMES = gpio.board_info()["led_names"]
def led_named(*names, fallback=0):
for n in names:
if n in LED_NAMES:
return gpio.led(LED_NAMES.index(n))
return gpio.led(fallback)
beacon_lamp = led_named(BEACON_LED if "RGB_GREEN" in LED_NAMES else "LED1") # แดงทั้งสองบอร์ด
เซนเซอร์ — บน Eva Kit ไม่ต้องเรียก sensors.init() และเรียกแล้วจะได้ OSError เพราะคอร์จอเป็นเจ้าของบัสเซนเซอร์ bmi270 capsense pot อ่านได้ทันทีผ่าน snapshot ที่คอร์จอเก็บไว้ให้ (การอ่านครั้งแรกหลังรีเซ็ตบน Eva อาจรอได้ถึงราว 16 วินาที) · บน Dev Kit init() ทำงานได้จริงแต่ก็ไม่ต้องเรียก เพราะเฟิร์มแวร์ปลุกเซนเซอร์ไว้ตั้งแต่บูต และ CM33 อ่าน IMU เองโดยตรง · sensors.snapshot() คืน dict รูปเดียวกันทั้งสองบอร์ด
หลอดไฟเตือนหน้างาน (beacon_lamp) ถูกเลือกตามชื่อ จาก gpio.board_info()["led_names"] ไม่ใช่ตามเลข เพราะดัชนี LED ต่างกันตามบอร์ด: RGB_RED บน Dev Kit เป็นดวงสีแดง ส่วนบน Eva ดวงที่ชื่อ RGB_RED เป็นสีน้ำเงิน (ชื่อกับสีไม่ตรงกัน — เป็นของจริงในเฟิร์มแวร์ อย่าไป "แก้") โครงจึงถามก่อนว่ามี RGB_GREEN ไหม: มี = Dev Kit ใช้ RGB_RED · ไม่มี = Eva ใช้ LED1 ซึ่งเป็นดวงแดง
โค้ดที่รันได้ทั้งสองบอร์ดไม่ได้เกิดจากการ "ไม่พูดถึงบอร์ด" แต่เกิดจากการถามบอร์ดว่ามันมีอะไร แล้วเลือกด้วยชื่อ

def decide(value):
if value > LIMIT: # ไล่จากเข้มที่สุดลงมาเสมอ สลับเมื่อไรจะไม่เข้า ALERT เลย
return "ALERT"
if value > WARN_LIMIT:
return "WARN"
return "OK"
def on_state_change(old, new, value):
pass # ทีมเขียนเอง: ตอนสถานะเปลี่ยนให้เกิดอะไร (นับจำนวนครั้ง จดเวลา สั่งของอย่างอื่น)
การตัดสินอยู่บนบอร์ด ไม่ใช่ที่ปลายทาง เหตุผลเดียวกับบทเรียน 1.4–1.6: เน็ตหลุดแล้วต้องยังตัดสินได้ และการส่งค่าดิบให้คลาวด์คิดแทนคือจ่ายค่าเน็ตเพื่อรอคำตอบที่ช้ากว่า
on_state_change() แยกเป็นฟังก์ชันต่างหาก เพราะ "สิ่งที่เกิดตอนสถานะเปลี่ยน" เป็นของทีมแต่ละทีม ไม่ใช่ของโครง — ทีมหนึ่งนับจำนวนครั้ง อีกทีมจดเวลาเพื่อคิดระยะเวลารวม
ระดับถัดไปที่เฉลยทำ: บังคับให้ค่าต้องเกินเกณฑ์ ติดกัน 3 รอบ ก่อนจึงเปลี่ยนสถานะจริง (CONFIRM_N) โครงเริ่มต้นเชื่อทันทีที่เห็นค่าเกินครั้งเดียว ซึ่งพอสำหรับให้ไฟล์รันได้ แต่จะโทรตามช่างเพราะรถบรรทุกวิ่งผ่าน
สองระดับใช้สอนได้ แต่ของที่ติดตั้งจริงเกือบทุกตัวมีการยืนยันซ้ำก่อนเชื่อด้วย
lbl_value = ui.Label("--", x=22, y=76, color=COL_TEXT, value=30) # ค่าหลัก
lbl_quality = ui.Label("รอค่าแรก", x=250, y=86, color=COL_DIM, value=16)
bar_value = ui.Bar(x=22, y=134, w=440, h=14, color=COL_RUN,
min=0, max=SCALE_MAX, value=0)
sc_value = ui.Scale(x=22, y=150, w=440, h=48, color=COL_TEXT, min=0, max=SCALE_MAX)
sc_value.ticks(10, 3) # 0 · 15 · 30 · 45 ไม่ทับกัน
led_ok = ui.Led(x=26, y=274, w=34, h=34, color=COL_OK, value=1)
led_warn = ui.Led(x=160, y=274, w=34, h=34, color=COL_WARN, value=0)
led_bad = ui.Led(x=320, y=274, w=34, h=34, color=COL_BAD, value=0)
btn_on = ui.Button("เปิดไฟเตือน", x=498, y=124, w=184, h=46, color=0x1B5E20, value=18)
btn_off = ui.Button("ปิดไฟเตือน", x=498, y=178, w=184, h=46, color=0x37474F, value=18)
สี่ข้อที่หน้าจอนี้ทำ และเป็นเกณฑ์ตรวจหน้าจอของทีมด้วย
| ข้อ | ทำอย่างไรในโค้ดนี้ |
|---|---|
| ค่าที่วัดได้ต้องมาพร้อมพิสัย | ui.Bar วางทับ ui.Scale — ตัวเลข 17.9 ลอย ๆ ไม่บอกว่าสูงไหม แต่แท่งที่อยู่บนไม้บรรทัด 0-45 บอกทันที |
| สถานะต้องเป็นไฟ ไม่ใช่ตัวอักษรสี | ui.Led สามดวง ติดทีละดวง — แปลงภาพเป็นขาวดำแล้วสีตัวอักษรหายหมด ไฟที่ติดกับไฟที่หรี่ยังแยกออก |
| ปุ่มเปิดกับปุ่มปิดต้องแยกกัน | btn_on กับ btn_off คนละตัว — ปุ่มเดียวสลับไปมาบอกไม่ได้ว่าตอนนี้อยู่สถานะไหน คนกดต้องเดา |
| ค่าที่แสดงต้องบอกคุณภาพตัวเอง | lbl_quality ขึ้นว่า "ค่าค้าง อ่านไม่ได้" เมื่อธง stale ถูกยก ไม่ใช่ค้างเลขเดิมไว้เฉย ๆ |
ui.Scale ไม่รับ .value() มันคือไม้บรรทัด ตัวที่ขยับคือ ui.Bar ที่วางทับ (แบบวงกลมมีเข็มจริง — บทเรียน 3.1–3.3) · ui.Led สั่ง .value(0) แล้ว หรี่ ไม่ใช่หาย ซึ่งตั้งใจ เพราะไฟที่หายไปตอนดับ ทำให้คนดูแยกไม่ออกว่าดับหรือจอเสีย
หน้าจอที่ดีตอบได้ใน 2 วินาทีว่า ตอนนี้ปกติหรือไม่ปกติ ตัวเลขละเอียดเป็นเรื่องรอง — คนหน้างานมองผ่านหน้ากากเชื่อมและถือของอยู่สองมือ
บรรทัด lbl_net มีอยู่เพราะจอต้องบอกความจริงเรื่องการเชื่อมต่อด้วย ไม่ใช่โชว์แต่ตัวเลขสวย ๆ ราวกับทุกอย่างปกติทั้งที่ส่งอะไรไม่ออกมาสิบนาทีแล้ว

ถามตัวเองว่า "ถ้าจอนี้ติดอยู่หน้าเครื่องจักรจริง คนเดินผ่านจะเข้าใจใน 2 วินาทีไหม"
# สร้างพร้อมหน้าจอแล้วซ่อนไว้ ไม่ใช่สร้างตอนกด - แฮนเดิลมีจำกัด และการสร้างของ
# ตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · กล่องวางกลางจอ
box = ui.MsgBox("ปิดไฟเตือน\nไฟหน้างานจะดับทันที", x=112, y=96, w=568, h=136,
color=COL_CARD)
box.hide()
btn_yes = ui.Button("ยืนยัน", x=144, y=248, w=200, h=88, color=0x3A4150, value=20)
btn_no = ui.Button("ยกเลิก", x=376, y=248, w=200, h=88, color=0x3A4150, value=20)
btn_yes.hide()
btn_no.hide()
...
elif ev["handle"] == btn_off.id() and not asking:
asking = True # เปิดไม่ต้องถาม เพราะย้อนกลับได้ทันที
box.show()
btn_yes.show()
btn_no.show()
elif ev["handle"] == btn_yes.id() and asking:
asking = False
beacon(False) # ของจริงกับจอขยับพร้อมกันในฟังก์ชันเดียว
box.hide()
btn_yes.hide()
btn_no.hide()
สองเรื่องที่ต้องรู้ก่อนใช้ ui.MsgBox
ui.Button เท่านั้น ถ้าวางปุ่มของกล่องไว้แล้วรอให้คนกด จะได้ปุ่มตายบนจอ และคนกดจะสรุปว่าเครื่องแฮงก์ — ใช้ ui.Button จริงสองตัวเป็นคำตอบแทนmodui.c เฟิร์มแวร์ 2026-08-20 ขึ้นไปส่งส่วนที่เกินซ้ำทาง .text() ได้ถึง 126 ไบต์ ยังต้องยืนยันบนบอร์ด · เขียนให้สั้นไว้ก่อนจึงปลอดภัยทุกรุ่น)คำยืนยันต้องบอก สิ่งที่จะเกิด ไม่ใช่ถามว่า "ยืนยันไหม" — เทียบสองประโยคนี้ตอนตีสามที่หน้างาน: "ยืนยันหรือไม่" กับ "ไฟหน้างานจะดับทันที" · ทีมที่ทำหน้าจอสั่งงานได้ ต้องตอบให้ได้ว่า "ถ้ากดผิดจะเกิดอะไร และย้อนกลับได้ไหม"
sec = now // 1000 # ประตูเดียว: วินาทีเปลี่ยนหรือยัง
if sec == last_sec:
return # แถบกับไฟขยับไปแล้วข้างบน ส่วนตัวเลขรอรอบหน้า
last_sec = sec
lbl_value.text("{:.1f} {}".format(value, UNIT))
ลูปเดินทุก 200 ms แต่ตัวเลขที่กระพริบห้าครั้งต่อวินาทีอ่านไม่ทัน — แถบกับไฟขยับได้ทุกรอบ เพราะตาอ่านรูปทรงได้เร็วกว่าตัวเลข ส่วนป้ายสถานะเขียนตอน เปลี่ยน ไม่ใช่ตอนถึงรอบวินาที ไม่งั้นป้ายกับไฟจะไม่ตรงกันได้นานถึงหนึ่งวินาที ซึ่งคนดูจะอ่านว่าจอเพี้ยน
สามจังหวะในฟังก์ชัน show() เดียวกัน: ทุกรอบ — แถบค่าและไฟสามดวง · ตอนเปลี่ยน — ป้ายสถานะและป้าย "ค้างอยู่" · วินาทีละครั้ง — ตัวเลขค่า ป้ายคุณภาพ สถานะเน็ต และตัวนับใบที่ส่ง
ตัวเลขที่กระพริบเร็วกว่าคนอ่านทัน ไม่มีใครได้ประโยชน์จากมัน — และตำแหน่งของมันต้องคงที่เสมอ
def send(value, state, kind):
if not mqtt.is_connected(): # เช็กสายก่อนส่งเสมอ
return False
mqtt.publish(TOPIC, payload(value, state, kind))
return True
def go_online():
if not wifi.is_connected():
if not wifi.connect(WIFI_SSID, WIFI_PASS):
return False
return mqtt.connect(BROKER, port=1883, client_id=DEVICE_ID)
if online and not mqtt.is_connected():
online = False
t_retry = now
if (not online) and time.ticks_diff(now, t_retry) >= RETRY_MS:
online = go_online() # นัดเวลาลองใหม่ ไม่ต่อรัว ๆ ในลูป
t_retry = now
สามอย่างที่ต้องสังเกต: send() คืน True/False ให้ผู้เรียกรู้ผล ไม่ใช่เงียบหาย · การต่อใหม่ถูกนัดเวลาไว้ ไม่ใช่พยายามทุกรอบลูป · และไม่ว่าเน็ตเป็นอย่างไร ลูปยังเดินครบทุกรอบ
client_id=DEVICE_ID สำคัญกว่าที่คิด — บน broker สาธารณะ ถ้าสองบอร์ดใช้ client id เดียวกัน จะเตะกันหลุดสลับไปมาเป็นลูป
ยกระดับได้ด้วย
tesaiot.connect()ของบทเรียน 4.7–4.9 (TLS 8884) แต่ต้อง provision ตัวตนของอุปกรณ์ในบัญชี TESAIoT Platform ก่อน จึงไม่อยู่ในโครงเริ่มต้น
ดูเพิ่ม (6 นาที): MQTT Essentials Part 7 — Quality of Service — HiveMQ — 5:41 — QoS 0/1/2 คือคำตอบระดับโพรโทคอลของคำถามเดียวกันนี้ และราคาที่ต้องจ่ายของแต่ละระดับ
จอกับ broker ต้องเป็นเส้นทางที่แยกกันได้ ถ้าฝั่งหนึ่งล้ม อีกฝั่งต้องไม่ล้มตาม
s12_capstone_starter.py ใน BENTO IDEDEVICE_ID, WIFI_SSID, WIFI_PASS, TOPIC (ใช้รหัสที่ไม่ซ้ำใคร เช่น nok4821 ทั้งใน DEVICE_ID และ bento/<รหัส>/... กันชนกับคนอื่นบน broker สาธารณะ)bento/<รหัส>/# เพื่อดูข้อความของบอร์ดคุณkind: event ขึ้น brokerถ้าเชื่อมต่อไม่ผ่าน wifi.connect() อาจบล็อกอยู่นาน อย่าเพิ่งรีบกดรันซ้ำ รอให้มันตอบก่อน
รันโครงให้ผ่านตั้งแต่ยังไม่แก้อะไร คือการพิสูจน์ว่าพื้นดีก่อนขึ้นบ้าน
ต้องทำในบทเรียน · เปิดตามลำดับนี้ ทั้งชุดราว 28 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · แยกค่าที่วัดได้ออกจากสถานะ · 10 นาที | 01_state_machine.py |
วางช่อง Decide ของ canvas ทีมได้เป็นโครงจริง ไม่ใช่ if กระจายทั้งไฟล์ · จะเข้าใจว่าค่าที่วัดได้กับสถานะที่ตัดสินแล้วเป็นคนละของ ต้องแยกกันอยู่ |
| 2 · ยืนพื้นครบ N รอบก่อนจึงเชื่อ · 10 นาที | 02_confirm_n.py |
กันไม่ให้ค่ากระตุกวูบเดียวยิง alert ตอนตีสอง ซึ่งเป็นข้อที่กรรมการถามแน่ · จะเข้าใจว่าระดับใหม่ต้องยืนพื้นครบ N รอบก่อน จึงจะเชื่อได้ว่ามันเปลี่ยนจริง |
| 3 · ต่อใหม่แบบถอยห่างขึ้นเรื่อย ๆ · 8 นาที | 03_reconnect_backoff.py |
ทำให้ demo รอดตอนเน็ตห้องสะดุด และตอบได้ว่าทำไมไม่ต่อใหม่ทุกวินาที · จะเข้าใจว่าการต่อใหม่ต้องถอยห่างขึ้นเรื่อย ๆ ไม่ใช่รัวเท่าเดิมทุกครั้ง |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| ถอดเราเตอร์แล้วจอค้างไปด้วยทั้งเครื่อง | 05_hmi_survives_offline.py — แยกลูปจอออกจากลูปเครือข่าย จอต้องเดินต่อได้ |
| alert ยิงถี่จน broker เต็มและคนเลิกอ่าน | 04_heartbeat_and_alert.py — heartbeat กับ alert คนละจังหวะ พร้อมช่วงเว้นที่นับได้ |
| อยากให้แหล่งค่าเป็นเสียง ไม่ใช่ค่าจากเซนเซอร์ตรง ๆ | 02_mic_sound_level_meter.py — mic.level() ยุบคลื่นเสียงทั้งชุดเหลือตัวเลขเดียว แล้วครอบด้วย confirm-N ได้เหมือนกัน |
| ตอน demo จอนิ่งอยู่เฉย ๆ แล้วตอบกรรมการไม่ได้ว่าโปรแกรมยังวิ่งอยู่หรือค้างไปแล้ว | 02_heartbeat_liveness.py — ไฟที่กะพริบเป็นจังหวะพิสูจน์ว่าลูปยังหมุน ส่วนไฟที่ติดค้างพิสูจน์ได้แค่ว่ามีไฟเลี้ยง · จังหวะนับจากนาฬิกา ไม่ใช่จาก sleep ยาว ๆ ที่ยึดลูปไว้ |
อ่านเสริมนอกเวลา — เรื่องนี้เป็นไฟล์ของบทเรียน 3.4–3.6 ไม่ใช่ชิ้นส่วนของโครงวันนี้ และไม่อยู่ในเกณฑ์ผ่านของบทเรียน 5.1–5.3: 01_imu_vibration_monitor.py เป็นโจทย์ capstone ที่ทำจบได้จริงในหนึ่งบทเรียน — เฝ้าการสั่นเทียบเส้นฐานที่วัดเอง แล้วรายงานเป็นกี่เท่าของเส้นฐาน · เปิดตอนเลือกโจทย์ยังไม่ลงตัว
โจทย์ที่ต้องฟังเสียงทำได้แล้ว โมดูล mic เปิดไมโครโฟนจาก Python ได้ทั้งบนบอร์ดและในอีมูเลเตอร์ ทีมที่อยากให้แหล่งค่าเป็นเสียงจึงเอา mic.level() หรือ mic.peak() ไปเสียบช่อง Sense ของ canvas ได้ตรง ๆ · กฎ confirm-N ในไฟล์ที่ 2 ข้างบนใช้ครอบค่าจากไมค์ได้เหมือนกับค่าจากเซนเซอร์ตัวอื่นทุกประการ ตัวอย่างไมค์สามไฟล์ (02 03 07) อยู่ที่บทเรียน 3.7–3.9