ภาพพื้นหลังปกบทเรียน

บทเรียน 5.2 — โครงตั้งต้น: Sense Decide Show Send

Capstone · AIoT Mini-Product ของทีมเรา: จากโจทย์จริง สู่ของที่ใช้งานได้

โมดูล 5 — Capstone: AIoT Mini-Product

ต่อจากบทเรียน 5.1 — จากโจทย์จริงสู่แบบ: canvas schema และการออกแบบตอนพัง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

70% — มีให้แล้ว เฟิร์มแวร์อ่านเซนเซอร์และวาดจอ · โมดูล sensors dsp ui wifi mqtt โครง s12_capstone_starter.py ที่รันครบวงจรตั้งแต่ยังไม่แก้อะไร 30% — งานของทีม เลือกโจทย์ · ตั้งเกณฑ์ · ออกแบบจอ schema · ออฟไลน์ · เล่าให้เข้าใจ

สิ่งที่มีให้แล้ว (70%) — เฟิร์มแวร์อ่านเซนเซอร์และวาดจอ, โมดูล sensors/dsp/ui/wifi/mqtt, และโครง s12_capstone_starter.py ที่รันครบวงจรได้ตั้งแต่ยังไม่แก้อะไร

งานของทีม (30%) — เลือกโจทย์ · เลือกว่าวัดอะไร · ตั้งเกณฑ์ · ออกแบบสิ่งที่คนหน้างานเห็น · ออกแบบ schema · ตัดสินใจเรื่องออฟไลน์ · เล่าให้คนอื่นเข้าใจใน 10 นาที

30% ของวันนี้ไม่ได้วัดกันที่จำนวนบรรทัด มันวัดกันที่ คุณภาพของการตัดสินใจ ทุกอย่างข้างบนคือการตัดสินใจ ไม่ใช่การพิมพ์

ชุดบทเรียนก่อน ๆ เราวัดกันว่า "ทำให้มันทำงานได้ไหม" วันนี้เราวัดว่า "ทำไมถึงทำแบบนี้"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโครงเริ่มต้น — ท่าที่ 1 Sense

motion() ax ay az gx gy gz dsp.tilt() roll, pitch roll มาก่อนเสมอ dsp.EMA(0.2) กันค่ากระโดด ครั้งเดียวจนเตือนผิด ค่าเดียว 17.4 หกแกนเข้า ค่าเดียวออก — สามช่องที่เหลือของ canvas ทำงานกับค่าเดียวนั้นทั้งหมด ทีมความสั่นเปลี่ยนเป็นขนาดความเร่งรวม · ทีมการเคลื่อนย้ายใช้ bmm350.heading() — โครงที่เหลือไม่ต้องแตะ
# บน 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 ในบันทึกการเรียนยังกรอกไม่เสร็จ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ท่าที่ 1 ต่อ — โครงเดียว รันได้ทั้งสองบอร์ดโดยไม่ต้องแก้

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 ซึ่งเป็นดวงแดง

โค้ดที่รันได้ทั้งสองบอร์ดไม่ได้เกิดจากการ "ไม่พูดถึงบอร์ด" แต่เกิดจากการถามบอร์ดว่ามันมีอะไร แล้วเลือกด้วยชื่อ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโครงเริ่มต้น — ท่าที่ 2 Decide

ยืนยันติดกัน 3 รอบก่อนเปลี่ยนสถานะ — 3 x 200 ms = หกในสิบวินาที LIMIT วูบเดียว ไม่เปลี่ยนสถานะ เกินติดกัน 3 รอบ = ALERT

แผนภาพ edge computing: อุปกรณ์ปลายทางประมวลผลใกล้จุดเกิดเหตุ แล้วส่งเฉพาะผลขึ้นคลาวด์

ภาพ: Psenda38 / Wikimedia Commons — CC0 1.0 · การตัดสินอยู่บนบอร์ด ไม่ได้อยู่ที่ปลายทาง — เน็ตหลุดแล้วต้องยังตัดสินได้ และไม่ต้องจ่ายค่าเน็ตเพื่อรอคำตอบที่ช้ากว่า
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) โครงเริ่มต้นเชื่อทันทีที่เห็นค่าเกินครั้งเดียว ซึ่งพอสำหรับให้ไฟล์รันได้ แต่จะโทรตามช่างเพราะรถบรรทุกวิ่งผ่าน

สองระดับใช้สอนได้ แต่ของที่ติดตั้งจริงเกือบทุกตัวมีการยืนยันซ้ำก่อนเชื่อด้วย

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโครงเริ่มต้น — ท่าที่ 3 Show

ค่าที่วัดได้ เทียบกับเกณฑ์ 17.9 deg ค่าปกติ 0 15 30 45 สถานะที่ตัดสินแล้ว ปกติ เฝ้าระวัง ผิดปกติ ผิดปกติ ต้องมีคนไปดู รับทราบ ไฟเตือนหน้างาน ไฟติดอยู่ เปิดไฟเตือน ปิดไฟเตือน สั่งปิดต้องยืนยันก่อน online · ส่งแล้ว 12 ใบ ค่า + พิสัย Bar ทับ Scale สถานะเป็นไฟ ไม่ใช่ตัวอักษรสี เปิด/ปิด คนละปุ่ม 31 จากงบ 32 เพดาน 64 ตัว ถ้าจอนี้ติดอยู่หน้าเครื่องจักรจริง คนเดินผ่านจะเข้าใจใน 2 วินาทีไหม
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 ถูกยก ไม่ใช่ค้างเลขเดิมไว้เฉย ๆ
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

ui.Scale ไม่รับ .value() มันคือไม้บรรทัด ตัวที่ขยับคือ ui.Bar ที่วางทับ (แบบวงกลมมีเข็มจริง — บทเรียน 3.1–3.3) · ui.Led สั่ง .value(0) แล้ว หรี่ ไม่ใช่หาย ซึ่งตั้งใจ เพราะไฟที่หายไปตอนดับ ทำให้คนดูแยกไม่ออกว่าดับหรือจอเสีย

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

บรรทัด lbl_net มีอยู่เพราะจอต้องบอกความจริงเรื่องการเชื่อมต่อด้วย ไม่ใช่โชว์แต่ตัวเลขสวย ๆ ราวกับทุกอย่างปกติทั้งที่ส่งอะไรไม่ออกมาสิบนาทีแล้ว

ภาพเคลื่อนไหว LED กะพริบเป็นจังหวะคล้ายหัวใจเต้น

ภาพ: smial / Wikimedia Commons — CC0 1.0 — จังหวะกะพริบแบบหัวใจเต้น คือช่อง Show ที่ถูกที่สุดเท่าที่มี ใช้ยืนยันว่าลูปยังเดินอยู่แม้จอจะยังไม่มีข้อมูลอะไรใหม่ให้แสดง · ทีมที่ยังไม่มีอะไรจะโชว์ ให้เริ่มจากอันนี้ก่อน

ถามตัวเองว่า "ถ้าจอนี้ติดอยู่หน้าเครื่องจักรจริง คนเดินผ่านจะเข้าใจใน 2 วินาทีไหม"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ท่าที่ 3 ต่อ — คำสั่งที่ทำให้ของจริงขยับ ต้องยืนยันก่อน

# สร้างพร้อมหน้าจอแล้วซ่อนไว้ ไม่ใช่สร้างตอนกด - แฮนเดิลมีจำกัด และการสร้างของ
# ตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · กล่องวางกลางจอ
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

  1. ปุ่มในตัว MsgBox เองยังไม่ส่งเหตุการณ์กลับมาให้ Python เห็น เฟิร์มแวร์ผูก callback ไว้กับ ui.Button เท่านั้น ถ้าวางปุ่มของกล่องไว้แล้วรอให้คนกด จะได้ปุ่มตายบนจอ และคนกดจะสรุปว่าเครื่องแฮงก์ — ใช้ ui.Button จริงสองตัวเป็นคำตอบแทน
  2. ข้อความของ MsgBox เดินทางไปกับ CREATE ซึ่งพาได้ 95 ไบต์ ภาษาไทยตัวละ 3 ไบต์ แปลว่าหัวเรื่องบวกเนื้อความรวมกันได้ราว 31 ตัวอักษร ยาวกว่านั้นถูกตัดเงียบ ๆ ไม่มี error ให้จับ มีแต่ประโยคที่ขาดครึ่งบนจอ (ตามซอร์ส modui.c เฟิร์มแวร์ 2026-08-20 ขึ้นไปส่งส่วนที่เกินซ้ำทาง .text() ได้ถึง 126 ไบต์ ยังต้องยืนยันบนบอร์ด · เขียนให้สั้นไว้ก่อนจึงปลอดภัยทุกรุ่น)

คำยืนยันต้องบอก สิ่งที่จะเกิด ไม่ใช่ถามว่า "ยืนยันไหม" — เทียบสองประโยคนี้ตอนตีสามที่หน้างาน: "ยืนยันหรือไม่" กับ "ไฟหน้างานจะดับทันที" · ทีมที่ทำหน้าจอสั่งงานได้ ต้องตอบให้ได้ว่า "ถ้ากดผิดจะเกิดอะไร และย้อนกลับได้ไหม"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ท่าที่ 3 ต่อ — ตัวเลขที่คนต้องอ่าน เขียนใหม่ไม่เกินวินาทีละครั้ง

    sec = now // 1000              # ประตูเดียว: วินาทีเปลี่ยนหรือยัง
    if sec == last_sec:
        return                     # แถบกับไฟขยับไปแล้วข้างบน ส่วนตัวเลขรอรอบหน้า
    last_sec = sec
    lbl_value.text("{:.1f} {}".format(value, UNIT))

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

สามจังหวะในฟังก์ชัน show() เดียวกัน: ทุกรอบ — แถบค่าและไฟสามดวง · ตอนเปลี่ยน — ป้ายสถานะและป้าย "ค้างอยู่" · วินาทีละครั้ง — ตัวเลขค่า ป้ายคุณภาพ สถานะเน็ต และตัวนับใบที่ส่ง

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโครงเริ่มต้น — ท่าที่ 4 Send และท่าที่ 5 กันเน็ตหลุด

นัดเวลาลองใหม่ ไม่ใช่พยายามทุกรอบลูป ต่อรัว ๆ ทุกรอบลูป ลูปหน่วง จอกระตุก และก็ยังไม่ติดอยู่ดี RETRY_MS = 10 วินาที ลูปเดินครบทุกรอบ จอไม่ค้าง ต่อติดแล้วส่ง kind: back ทันที client_id = DEVICE_ID ต้องไม่ซ้ำ — สองบอร์ดใช้ id เดียวกันจะเตะกันหลุดสลับไปมาเป็นลูป
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 ก่อน จึงไม่อยู่ในโครงเริ่มต้น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ข้อมูลไหลไปทางไหน — ทั้งวงจรในภาพเดียว

BMI270 ทุก 200 ms EMA + tilt เหลือค่าเดียว decide() OK / ALERT จอ HMI เห็นทันที เสมอ MQTT 1883 เฉพาะเหตุการณ์ คนที่รับผิดชอบ ไปทำอะไรต่อ เน็ตหลุด: นับที่พลาดไว้ นัดต่อใหม่ทุก 10 วินาที แต่เส้นทางไปจอไม่ขาดตอน เส้นบนไม่พึ่งเน็ต เส้นล่างพึ่ง — ออกแบบให้ของสำคัญอยู่บนเส้นบน

ดูเพิ่ม (6 นาที): MQTT Essentials Part 7 — Quality of Service — HiveMQ — 5:41 — QoS 0/1/2 คือคำตอบระดับโพรโทคอลของคำถามเดียวกันนี้ และราคาที่ต้องจ่ายของแต่ละระดับ

จอกับ broker ต้องเป็นเส้นทางที่แยกกันได้ ถ้าฝั่งหนึ่งล้ม อีกฝั่งต้องไม่ล้มตาม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

1-2 · เตรียม เปิด Playground ค้างไว้ เปิดไฟล์ starter ใน IDE 3-4 · รันโครงเปล่า แก้ CONFIG ให้เป็นของทีม ยังไม่ต้องแก้ตรรกะอะไร 5-6 · ดูปลายทาง MQTT Explorer subscribe เอียงเกิน 15 องศาค้างไว้ 7 · ทดสอบการพัง ปิด WiFi หรือถอด router จอวาดต่อ ขึ้น offline 8 · ค่อยแทนที่ส่วนที่เขียนว่า "ทีมเขียนเอง" ทีละจุด แล้วรันดูทุกครั้ง wifi.connect() บล็อกได้นาน อย่าเพิ่งรีบกดรันซ้ำ รอให้มันตอบก่อน
  1. บนจอบอร์ด แตะการ์ด BENTO Playground แล้วค้างหน้านี้ไว้
  2. เปิด s12_capstone_starter.py ใน BENTO IDE
  3. แก้บล็อก CONFIG ให้เป็นของคุณ: DEVICE_ID, WIFI_SSID, WIFI_PASS, TOPIC (ใช้รหัสที่ไม่ซ้ำใคร เช่น nok4821 ทั้งใน DEVICE_ID และ bento/<รหัส>/... กันชนกับคนอื่นบน broker สาธารณะ)
  4. กด Program to Device แล้วดูจอบอร์ด — ยังไม่ต้องแก้ตรรกะอะไร มันต้องรันได้แล้วตั้งแต่ตอนนี้
  5. เปิด MQTT Explorer บนคอม แล้ว subscribe bento/<รหัส>/# เพื่อดูข้อความของบอร์ดคุณ
  6. เอียงบอร์ดเกิน 15 องศาค้างไว้ ดูว่าจอเปลี่ยนสถานะและมีข้อความ kind: event ขึ้น broker
  7. ทดสอบการพัง: ปิด WiFi ที่เราต่อ (หรือถอด router) แล้วสังเกตว่าจอยังวาดต่อและขึ้น offline
  8. ค่อยเริ่มแทนที่ส่วนที่เขียนว่า "ทีมเขียนเอง" ทีละจุด แล้วรันดูทุกครั้ง

ถ้าเชื่อมต่อไม่ผ่าน wifi.connect() อาจบล็อกอยู่นาน อย่าเพิ่งรีบกดรันซ้ำ รอให้มันตอบก่อน

รันโครงให้ผ่านตั้งแต่ยังไม่แก้อะไร คือการพิสูจน์ว่าพื้นดีก่อนขึ้นบ้าน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ตัวอย่างของบทเรียน 5.1–5.3 — สามไฟล์แรกคือชุดที่ทำให้ demo วันนำเสนอไม่ล้ม

ต้องทำในบทเรียน · เปิดตามลำดับนี้ ทั้งชุดราว 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 ยาว ๆ ที่ยึดลูปไว้
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

ตัวอย่างของบทเรียน 5.1–5.3 (ต่อ) — อ่านเสริม และโจทย์ที่แหล่งค่าเป็นเสียง

อ่านเสริมนอกเวลา — เรื่องนี้เป็นไฟล์ของบทเรียน 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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0