บทเรียน 6.3 — ท่อสั่งการ: CONF_FLOOR, debounce, cooldown และ on_result

จาก verdict สู่ action จริง — RGB · เสียง · log

โมดูล 6 — แอป Edge AI

โมดูล 6 · Pillar 5 Apps

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

MicroPython บนบอร์ด BENTO (PSoC Edge · Cortex-M55 + Ethos-U55 NPU)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เปิดบทเรียนด้วยของจริงก่อน

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

รันท่อสั่งการก่อน ไอ -> ไฟแดง + เสียง แกะดูข้างใน 4 ด่านของท่อ เติม/จูนเอง NEED_HITS · cooldown ต่อยอด 6.5–6.6 ออกเน็ต

เปิด s16_action_pipeline_full.py รันเลย เลือกโมเดล Cough แล้วลองไอ — ไฟบนจอจะไล่จาก ฟ้า (เฝ้าดู) เป็น เหลือง (เจอแล้วแต่ยังไม่ชัวร์) แล้วค่อยเป็น แดง (ยิงเตือน) พร้อมเสียงและบรรทัด log

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เป้าหมายของชุดบทเรียนนี้

จบชุดบทเรียนนี้เราจะต่อ "ท่อสั่งการ" (action pipeline) ที่แปลง verdict ของโมเดลให้กลายเป็น action จริง อย่างมีวินัย:

  1. verdict → action — เอาผลจากโมเดล (result()) ไปสั่งการจริง: ไฟ RGB บนจอ · เสียง · log
  2. false positive คืออะไร และทำไมยิง action ตรงๆ จาก verdict ดิบถึงเตือนพร่ำเพรื่อ
  3. สามเกราะกันเตือนผิด — เกณฑ์ความมั่นใจ (CONF_FLOOR) · debounce (จับต่อเนื่อง) · cooldown (เว้นช่วง)
  4. smoothing ด้วย dsp.EMA เพื่อกดสัญญาณกระตุกชั่ววูบ
  5. edge_ai.on_result(cb) — รับ verdict แบบ event-driven ต่างจากการ poll ยังไง เลือกอันไหน
  6. ลงมือ: เติม s16_action_pipeline.py ให้ครบ 4 ด่านของท่อ แล้วจูนจนกัน false positive ได้

ปลายทางของวันนี้: ทำเสียง/ท่าให้โมเดลจับคลาสเป้าหมายได้ ต่อเนื่องนานพอ จอถึงจะยิงไฟแดง + เสียง + log — ไอแวบเดียวไม่นับ

บทเรียน 6.1–6.2 เราทำแอปต่อโมเดลเดี่ยว ชุดบทเรียนนี้เราต่อ "หลังบ้าน" ของแอปนั้น: ชั้นตัดสินใจที่ทำให้มันเชื่อถือได้พอจะเอาไปใช้จริง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ย้อนดูบทเรียน 1.1–1.3 — เราหยุดไว้ตรง "อ่านผล"

ชุดบทเรียนแรกเราเรียก 4 คำสั่งของ edge_ai แล้วเอาคลาสที่ชนะขึ้นจอ จบแค่นั้น — อ่านผลเป็น แต่ยัง ไม่สั่งการ

บทเรียน 1.1–1.3 result() -> ขึ้นจอ บทเรียน 6.3–6.4 (วันนี้) result() -> ตัดสินใจ -> action บทเรียน 6.5–6.6 action -> WiFi/MQTT ออกเน็ต ของใหม่ชุดบทเรียนนี้ = สองกล่องกลาง "ตัดสินใจ" กับ "action"
  • บทเรียน 1.1–1.3: models → select → result → stop (อ่านผลออก แต่ผลนั้นไม่ได้ไปทำอะไรต่อ)
  • ชุดบทเรียนนี้: ใส่ ชั้นตัดสินใจ คั่นระหว่าง "อ่านผล" กับ "สั่งการ" — เพราะ verdict ดิบเชื่อทั้งดุ้นไม่ได้

ในโลกจริง แอป Edge AI ที่ขายได้ ต่างจาก demo ตรง "ชั้นตัดสินใจ" นี่แหละ demo ยิงทุก verdict ก็ดูเท่ในวิดีโอ แต่ของจริงต้องไม่ปลุกคนทั้งบ้านเพราะแมวเดินผ่าน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ชุดบทเรียนนี้อยู่ตรงไหนของวงจร

เราอยู่ที่ขั้นสุดท้ายของวงจรชีวิตข้อมูล — Apps (ขั้น 5) — และเป็นชุดบทเรียนกลางของ โมดูล 6 (Apps I·II·III)

1 · DAQ โมดูล 2 2 · Processing โมดูล 3 3 · Analysis โมดูล 4 4 · Training โมดูล 5 5 · Apps (เราอยู่นี่) 6.1–6.2 · 6.3–6.4 · 6.5–6.6 6.3–6.4 = ท่อสั่งการ + debounce
  • บทเรียน 6.1–6.2 (Apps I) — แอปต่อโมเดลเดี่ยว: เลือกโมเดล อ่าน scores/latency โชว์สวยๆ
  • บทเรียน 6.3–6.4 (Apps II · วันนี้) — เอา verdict ไปสั่งการจริง + ชั้นกัน false positive
  • บทเรียน 6.5–6.6 (Apps III) — รวม verdict กับเซนเซอร์ดิบ (fusion) แล้วส่งออกเน็ตด้วย WiFi/MQTT

action pipeline ที่เราต่อวันนี้ คือกระดูกสันหลังของทุกแอป Edge AI จริง — บทเรียน 6.5–6.6 แค่ต่อปลายท่อออกไปที่คลาวด์

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

verdict → action: ท่อ 4 ด่าน

หัวใจของชุดบทเรียนนี้คือมองการสั่งการเป็น ท่อ (pipeline) ที่ verdict ต้องผ่านทีละด่านก่อนจะได้ยิง action

verdict ดิบ result(): label+conf 1 · กรอง conf ≥ CONF_FLOOR + ตรงคลาสเป้าหมาย 2 · debounce จับต่อเนื่อง streak ≥ NEED_HITS 3 · cooldown พ้นช่วงเว้น กันยิงรัว 4 · action RGB · เสียง · log fire_action() verdict หลุดด่านไหน = ไม่ยิง (ตกด่านคือเรื่องดี — นั่นคือมันกัน false positive ให้เรา)

อ่านท่อนี้ให้ขึ้นใจ เดี๋ยว 4 ช่องที่เราเติมในไฟล์ฝึก คือด่าน 1→4 นี้เป๊ะ ไล่จากซ้ายไปขวา

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ปัญหาที่ต้องแก้ — โมเดลกระพริบ = false positive

ถ้าเรายิง action ทุกครั้งที่ label == "cough" โดยไม่กรองอะไรเลย จะเกิดอะไรขึ้น? ลองดูสัญญาณจริง:

เวลา → conf CONF_FLOOR แหลมเดี่ยวสั้นๆ = เดาผิดชั่ววูบ (false positive) ของจริง = ค้างสูงต่อเนื่อง
  • ยอดแหลมเดี่ยวๆ (สีส้ม) คือโมเดล "เดาผิดชั่ววูบ" — คลาสกระพริบขึ้นเหนือเส้นแป๊บเดียวแล้วตกลง
  • ของจริง (แถบเขียว) คือคลาสเป้าหมายค้างสูง ต่อเนื่องหลายเฟรม
  • ถ้ายิงทุก verdict ที่เกินเส้น เราจะเตือน 3 ครั้งจากยอดแหลมปลอม + 1 ครั้งจากของจริง = เตือนผิด 3 ครั้ง

นี่คือปัญหาเดียวกับปุ่มกดจริงในวงจรดิจิทัลที่เรียกว่า switch bounce — หน้าสัมผัสสั่นเป็นสิบครั้งใน 1 การกด วิศวกรแก้ด้วยเทคนิคชื่อ debounce เราจะยืมมาใช้กับ verdict ของโมเดลตรงๆ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เกราะชั้นที่ 1 — เกณฑ์ความมั่นใจ (CONF_FLOOR)

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

hit = (r['label'] == TARGET_CLASS and r['conf'] >= edge_ai.CONF_FLOOR)
#      └ ตรงคลาสที่เราเฝ้าไหม          └ มั่นใจถึงเกณฑ์ไหม (0.50)
  • edge_ai.CONF_FLOOR = 0.50 — ต่ำกว่านี้ถือว่า "ยังไม่ชัวร์" อย่าเพิ่งนับเป็นการเจอ
  • ด่านนี้ตัดยอดแหลมที่ เตี้ย ทิ้งได้ แต่ยอดแหลมที่ สูงชั่ววูบ ยังหลุดผ่านมาได้ — จึงต้องมีด่าน 2 ต่อ
  • hit เป็นแค่ boolean ของ "เฟรมนี้" — ยังไม่ใช่การตัดสินใจยิง แค่บอกว่าเฟรมนี้นับเป็นการเจอ

เกณฑ์นี้จูนได้ ตั้งสูง (เช่น 0.70) = เตือนผิดน้อยลงแต่พลาดของจริงง่ายขึ้น · ตั้งต่ำ = ไวขึ้นแต่หลอกง่ายขึ้น เราจะได้เล่นกับ trade-off นี้ตอนจูน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เกราะชั้นที่ 2 — debounce (ต้องจับต่อเนื่อง)

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

เฟรม: hit - streak=0 · ไม่ยิง hit hit hit streak=3 ≥ NEED_HITS → ยิง! แหลมเดี่ยว: streak รีเซ็ตทันทีที่หลุด ค้างต่อเนื่อง 3 เฟรม: ผ่านด่าน debounce
if hit:
    streak += 1          # เจอต่อเนื่อง เพิ่มตัวนับ
else:
    streak = 0           # หลุดเมื่อไร รีเซ็ตทันที (ยอดแหลมเดี่ยวถูกล้างตรงนี้)
ready = streak >= NEED_HITS

streak ทำงานเหมือน "ต้องกดค้าง" ไม่ใช่ "แตะแล้วปล่อย" — สัญญาณรบกวนชั่ววูบผ่านไม่ได้ เพราะมันไม่ค้าง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เกราะชั้นที่ 3 — cooldown (เว้นช่วงกันยิงรัว)

debounce กันยอดแหลมได้ แต่ยังมีอีกปัญหา: ถ้าคลาสเป้าหมายค้างสูง ยาว (เช่น ไอเป็นชุด) streak จะครบซ้ำๆ ทุกไม่กี่เฟรม → ยิงรัวเป็นสิบครั้ง เราจึงเพิ่ม ช่วงเว้น (refractory period)

ยิง #1 COOLDOWN_MS (เช่น 3000 ms) — ยิงไม่ได้ ยิง #2 เว้นอีกช่วง ยิง #3
now = time.ticks_ms()
cooled = time.ticks_diff(now, last_fire) >= COOLDOWN_MS
if ready and cooled:            # ครบ debounce "และ" พ้นช่วงเว้น
    fire_action(r['conf'])
    last_fire = now             # เริ่มนับ cooldown ใหม่
    streak = 0

ใช้ time.ticks_ms() คู่ time.ticks_diff() เสมอ อย่าลบเวลาตรงๆ เพราะตัวนับ ms มีวันวนกลับ (overflow) ticks_diff จัดการให้ถูกต้อง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เกราะเสริม — smoothing ด้วย dsp.EMA

ในไฟล์ฉบับเต็ม เราเพิ่มอีกชั้น: ก่อนตัดสินใจ เอาคะแนนคลาสเป้าหมายมา "ปรับให้เนียน" ด้วย EMA (ตัวเดียวกับที่เรียนในบทเรียน 4.1–4.2)

import dsp
smoother = dsp.EMA(alpha=0.35)        # สร้างครั้งเดียวก่อนลูป
...
raw = r['scores'][target_idx]          # คะแนนดิบของคลาสเป้าหมาย
sm  = smoother.update(raw)             # ค่าที่เนียนแล้ว
smooth_ok = sm >= edge_ai.CONF_FLOOR   # ใช้ค่าเนียนเป็นด่านเสริม
  • EMA ถ่วงน้ำหนักค่าเก่ากับค่าใหม่: y = alpha*x + (1-alpha)*y_prev — ยอดแหลมเดี่ยวถูกกดลง
  • alpha ต่ำ = เนียนมากแต่ตอบช้า · สูง = ไวแต่กระตุก (0.35 คือจุดกลางๆ ที่ใช้ได้ดี)
  • debounce กับ smoothing แก้คนละมุม: debounce นับ "จำนวนเฟรม" · smoothing กด "ขนาดของค่า" — ใช้คู่กันยิ่งแน่น

ไม่จำเป็นต้องมีทั้งสอง ไฟล์ฝึกใช้แค่ debounce ก็กัน false positive ได้แล้ว EMA คือของแถมในฉบับเต็มที่ทำให้เนียนขึ้นอีกขั้น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตของ EMA — สูตรเดียว เข้าใจทั้งชั้น smoothing

หัวใจของ smoothing เขียนเป็นสมการเดียว ค่าที่เนียนแล้วรอบนี้ = ผสมค่าดิบรอบนี้กับค่าที่เนียนแล้วรอบก่อน

yt=α xt+(1−α) yt−1y_t = \alpha\,x_t + (1-\alpha)\,y_{t-1}

อ่านทีละตัว (ภาษาคน):

  • xtx_t — คะแนนดิบของคลาสเป้าหมาย ณ เฟรมนี้ (ค่าที่ได้จาก r['scores'][target_idx])
  • yty_t — คะแนน หลังปรับให้เนียน ที่เราจะเอาไปตัดสินใจ
  • yt−1y_{t-1} — ค่าเนียนของ เฟรมก่อนหน้า (ความทรงจำของตัวกรอง)
  • α\alpha — น้ำหนักของค่าใหม่ อยู่ระหว่าง 00 ถึง 11 (ในโค้ดคือ alpha=0.35)

ลองแทนค่าให้เห็นภาพ: ถ้า α=0.35\alpha = 0.35 และตอนนี้ yt−1=0.20y_{t-1}=0.20 แล้วมียอดแหลมเดี่ยว xt=0.90x_t = 0.90 พุ่งขึ้นมา

yt=0.35(0.90)+0.65(0.20)=0.315+0.130=0.445y_t = 0.35(0.90) + 0.65(0.20) = 0.315 + 0.130 = 0.445

  • ยอดแหลม 0.900.90 ถูก กดเหลือ 0.4450.445 ในเฟรมเดียว — ยังไม่ทะลุ CONF_FLOOR = 0.50 ด้วยซ้ำ นั่นคือเหตุผลที่ EMA กันแหลมปลอมได้
  • ถ้าคะแนนสูงจริง หลายเฟรมติด yty_t จะไต่ขึ้นเรื่อยๆ จนเกินเส้น — ของจริงที่ค้างนานถึงจะผ่าน

ทำไม EMA ถึงเบา (เหมาะกับบอร์ด): เก็บสถานะแค่ตัวเดียว (yt−1y_{t-1}) ไม่ต้องเก็บ buffer ย้อนหลัง — คนละเรื่องกับ moving-average ที่ต้องจำ NN ค่า นี่คือเหตุผลที่มันวิ่งได้สบายบน Cortex-M55 ทุกเฟรม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตของ "ยิงตอนขอบขึ้น" — rising edge past threshold

debounce ที่เราต่อ ไม่ได้ยิงตอนคะแนน "สูง" เฉยๆ — มันยิงตอนคะแนน ข้ามเส้นจากล่างขึ้นบน (rising edge) แล้วค้างครบจำนวนเฟรม เขียนเป็นสองเงื่อนไข

ให้ sts_t = คะแนนที่เนียนแล้ว, θ\theta = เกณฑ์ (CONF_FLOOR), HH = NEED_HITS

1) นับความต่อเนื่อง (streak):

ct={ct−1+1,st≥θ(hit)0,st<θ(miss)c_t = \begin{cases} c_{t-1} + 1, & s_t \ge \theta \quad (\text{hit}) \\ 0, & s_t < \theta \quad (\text{miss}) \end{cases}

2) ยิงเฉพาะ "ขอบขึ้น" ของความพร้อม — ครบเกณฑ์รอบนี้ แต่รอบก่อนยังไม่ครบ:

firet=[ ct≥H ]  ∧  [ ct−1<H ]  ∧  [ t−tlast≥Tcool ]\text{fire}_t = [\,c_t \ge H\,] \;\wedge\; [\,c_{t-1} < H\,] \;\wedge\; [\,t - t_\text{last} \ge T_\text{cool}\,]

อ่านเป็นภาษาคน:

  • [ ct≥H ][\,c_t \ge H\,] — จับคลาสเป้าหมายได้ต่อเนื่องครบแล้ว (ผ่าน debounce)
  • [ ct−1<H ][\,c_{t-1} < H\,] — เพิ่งครบเดี๋ยวนี้ ไม่ใช่ครบมาตั้งนานแล้ว → กันยิงซ้ำทุกเฟรมตอนคะแนนค้างยาว
  • [ t−tlast≥Tcool ][\,t - t_\text{last} \ge T_\text{cool}\,] — พ้นช่วง cooldown จากครั้งก่อน (COOLDOWN_MS)
s(t) θ ขอบขึ้น = ยิงที่นี่ ค้างเหนือเส้นต่อไป = ไม่ยิงซ้ำ (เพราะ c(t-1) ครบแล้ว)

ทำไมต้อง "ขอบขึ้น" ไม่ใช่ "อยู่เหนือเส้น": ถ้ายิงทุกเฟรมที่ st≥θs_t \ge \theta พอคะแนนค้างสูงยาวๆ จะยิงรัวเป็นสิบครั้ง เงื่อนไข ct−1<Hc_{t-1} < H ทำให้ยิง ครั้งเดียวต่อหนึ่งเหตุการณ์ — นี่คือหลักการเดียวกับ edge-triggered interrupt ในโลก embedded

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

รู้จัก edge_ai.on_result(cb) — รับผลแบบ event-driven

จนถึงตอนนี้เรา poll — เรียก result() เองทุกรอบลูป แต่ edge_ai มีอีกวิธี: ให้เฟิร์มแวร์ โทรกลับ หาเราเมื่อมีเหตุการณ์

def on_change(r):                     # เฟิร์มแวร์เรียกเมื่อคลาสเปลี่ยน (และทวนราววินาทีละครั้ง)
    print("คลาสใหม่:", r['label'], "%.0f%%" % (r['conf']*100))

edge_ai.on_result(on_change)          # ลงทะเบียน callback
...
edge_ai.on_result(None)               # ถอนตอนจบ (คู่กับตอนลงทะเบียน)
  • cb(dict) รับ dict verdict แบบเดียวกับ result() — รันใน MicroPython scheduler context ปลอดภัยกับ print/UI
  • เฟิร์มแวร์ยิง callback ให้ ทันทีที่คลาสที่ชนะเปลี่ยน และทวนคลาสเดิมราววินาทีละครั้ง ไม่ใช่ทุกเฟรม
  • เราไม่ต้องเช็ก seq เอง ไม่ต้องวนถาม — เหมือน "รอสายเข้า" แทน "โทรถามซ้ำๆ"

จากซอร์สเฟิร์มแวร์ (ยังไม่เปิดเผย): callback ผูกกับ INTENT event ที่ส่งตอนคลาสเปลี่ยน และส่งซ้ำคลาสเดิมห่างกันอย่างน้อยราว 1 วินาที — ไม่ใช่ทุก result

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

on_result กับ poll — เลือกอันไหน

สองวิธีนี้ไม่ได้มีอันถูกอันผิด มันเหมาะกับงานคนละแบบ ชุดบทเรียนนี้เราใช้ ทั้งคู่ ในฉบับเต็ม

result() (poll) on_result(cb) (event)
ใครเป็นคนเรียก เราเรียกเองทุกรอบลูป เฟิร์มแวร์เรียกให้ตอนคลาสเปลี่ยน (และทวนราววินาทีละครั้ง)
ได้ผลบ่อยแค่ไหน ทุกเฟรม (นับ streak ได้) ตอนเปลี่ยนคลาส + ทวนราววินาทีละครั้ง
เหมาะกับ debounce / smoothing (ต้องนับต่อเฟรม) log การเปลี่ยนคลาส · action ง่ายๆ
ต้องเช็ก seq เอง ใช่ ไม่ต้อง
  • debounce ต้องใช้ poll เพราะต้องนับว่าคลาสค้างมากี่เฟรม — event ที่ยิงเฉพาะตอน "เปลี่ยน" นับต่อเนื่องไม่ได้
  • log เหมาะกับ on_result เพราะเราอยากบันทึกเฉพาะ "ตอนคลาสเปลี่ยน" ไม่ใช่ทุกเฟรม (จำคลาสล่าสุดไว้ แล้วข้ามรอบที่เฟิร์มแวร์ทวนคลาสเดิม)
  • ในฉบับเต็ม: ลูปหลัก poll เพื่อ debounce+action · on_result แยกไปทำ log การเปลี่ยนคลาส — ต่างคนต่างอ่านสถานะ ไม่ชนกัน

จำหลักไว้: งานที่ต้องนับเวลา/จำนวนเฟรม → poll · งานที่แค่ตอบสนองต่อ "เหตุการณ์" → callback นี่คือ pattern ที่เจอทั่วงาน embedded

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ช่องทางของ action — RGB · เสียง · log

พอ verdict ผ่านครบ 4 ด่าน เราจะสั่งการออกไป 3 ช่องทางพร้อมกัน แต่ละช่องสื่อสารกับคนคนละแบบ

fire_action() ผ่านครบ 4 ด่านแล้ว RGB (ภาพ) light.color(RED) เสียง ui.tone / ui.sfx log lcd.console + counter
  • RGB เตือนคนที่มองเห็น (สถานะปัจจุบัน) · เสียง เตือนคนที่ไม่ได้มอง · log เก็บหลักฐานไว้ตรวจย้อนหลัง
  • ในงานจริง action อาจเป็น เปิดรีเลย์ · สั่นมอเตอร์ · ส่ง MQTT — โครงเดียวกัน แค่เปลี่ยนปลายท่อ

แยกฟังก์ชัน fire_action() ออกมาต่างหาก ทำให้ "จะทำอะไรตอนยิง" แก้ที่เดียวจบ ไม่ปนกับตรรกะ debounce

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

action ช่อง RGB — ไฟสถานะบนจอ

บอร์ดไม่มี LED RGB แยกให้สั่งตรงๆ เราจึงทำ "ไฟ RGB" เป็น การ์ดสีบนจอ ที่เปลี่ยนสีตามสถานะของท่อ — เห็นชัดว่าตอนนี้ pipeline อยู่ด่านไหน

light = ui.Panel(x=380, y=48, w=120, h=120, color=LIGHT_OFF)  # สร้างครั้งเดียว
...
light.color(CYAN)     # กำลังเฝ้าดู (ยังไม่เจอเป้าหมาย)
light.color(AMBER)    # เจอแล้วแต่ streak ยังไม่ครบ
light.color(RED)      # ยิง! ผ่านครบทุกด่าน
สี สถานะ pipeline หมายความว่า
เทา (off) ยังไม่เริ่ม รอผลแรก
ฟ้า (CYAN) watching เฝ้าดูอยู่ ไม่เจอคลาสเป้าหมาย
เหลือง (AMBER) streak กำลังนับ เจอแล้ว แต่ยังไม่ต่อเนื่องพอ
แดง (RED) ALERT ยิง action แล้ว

ไล่สีจากฟ้า→เหลือง→แดง ทำให้ผู้ใช้ "เห็น" ว่าท่อกำลังกันหรือกำลังจะยิง — โปร่งใสกว่าไฟติด/ดับเฉยๆ และช่วยเราดีบักตอนจูนด้วย

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

action ช่องเสียง — ui.tone / ui.sfx

เสียงเตือนคนที่ไม่ได้จ้องจอ BENTO มีสองทางให้เลือก ทั้งคู่เป็น API จริงบนบอร์ด

if hasattr(ui, "tone"):
    ui.tone(76, ui.WAVE_SINE, 140, 160)   # โน้ต MIDI 76 · รูปคลื่น · ความแรง (velocity) · ยาว 160 ms
elif hasattr(ui, "sfx"):
    ui.sfx(ui.SFX_UI_DENY)                # เอฟเฟกต์สำเร็จรูป
  • ui.tone(note, wave, ...) — โน้ตดนตรีสั้นๆ กำหนดความสูงต่ำได้ (โน้ต MIDI 60 = โดกลาง, 76 = มีสูง)
  • ui.sfx(id) — เสียงเอฟเฟกต์สำเร็จรูป เช่น ui.SFX_UI_DENY, ui.SFX_UI_SELECT
  • ห่อด้วย hasattr(ui, "tone") เสมอ — บางบอร์ด/Emulator อาจไม่มีลำโพง โค้ดจะได้ไม่พังทั้งแอปเพราะเรื่องเสียง

เลือกเสียงให้ตรงความหมาย: เตือนภัยควรเป็นเสียงสูง-สะดุด ไม่ใช่เสียงนุ่มๆ ที่คนมองข้าม — action ที่ดีต้อง "สื่อสาร" ไม่ใช่แค่ "ดัง"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

action ช่อง log — บันทึกไว้ตรวจย้อนหลัง

action ที่มองไม่เห็นตอนเกิด แต่สำคัญที่สุดสำหรับงานจริง: log — เพราะเราต้องตอบได้ว่า "มันเตือนกี่ครั้ง ตอนไหน มั่นใจเท่าไร"

alerts += 1                                        # ตัวนับจำนวนครั้ง
lcd.console('<span class=error> ALERT #%d @%dms: %s (conf %.0f%%)</span>'
            % (alerts, time.ticks_ms(), TARGET_CLASS, r['conf'] * 100))
  • lcd.console(...) พิมพ์บรรทัด log พร้อม timestamp + ความมั่นใจ ณ ตอนยิง
  • ตัวนับ alerts โชว์บนจอ — เห็นภาพรวมว่าเตือนไปกี่ครั้งแล้ว
  • ในฉบับเต็มยังนับ blocked = จำนวนครั้งที่ pipeline กันการยิงซ้ำไว้ — ทำให้เห็นว่าเกราะทำงานจริง

log คือความต่างระหว่าง "ของเล่น" กับ "ของใช้งาน" ถ้าเตือนผิดกลางดึก คุณต้องเปิด log มาดูได้ว่าเกิดจากอะไร บทเรียน 6.5–6.6 เราจะส่ง log นี้ขึ้นคลาวด์

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0