Skip to content

The action pipeline: CONF_FLOOR, debounce, cooldown and on_result

Module 6 — Edge AI apps · Slides: slides.md · Module overview · Course page

Turn counting into disciplined, real action with a four-stage pipeline: a CONF_FLOOR filter, a debounce needing a streak, a cooldown against rapid re-firing, and the action itself through an RGB light, sound and a log — plus EMA smoothing, the maths of firing on the rising edge, and choosing between polling and on_result.

By the end of this lesson, you will:

  1. Describe the four-stage pipeline (filter → debounce → cooldown → action), and say which kind of false positive each stage stops.
  2. Compute one EMA step by hand from y_t = αx_t + (1 − α)y_{t−1}, and explain why it flattens a single spike while keeping only one value of state.
  3. Apply the firing rule fire = [c_t ≥ H] ∧ [c_{t−1} < H] ∧ [t − t_last ≥ T_cool] to a given frame sequence, and explain why time must be measured with ticks_diff.
  4. Choose between polling with result() and a callback with on_result() for a given task, knowing the callback arrives on a class change and repeats the same class about once a second.

You’ve been through lessons 6.1–6.2, and have a counting app using CONF_FLOOR and rising-edge counting, and remember dsp.EMA from lesson 4.1. Open the 20_confirmed_alert.py example in BENTO IDE.

  • Hardware: a TESAIoT Dev Kit board already flashed with BENTO’s MicroPython firmware, or the BENTO Emulator inside BENTO IDE — on the emulator, use the Motion model with the Shake button to practise the whole pipeline (the audio models on the emulator are simulated values where the event class never wins), and the on_result callback fires when the program calls result() or active().
  • Prior lesson: lesson 6.2 — Hands-on: your own focused app

Run s16_action_pipeline_full.py (in lesson 6.4), select the Cough model, and try coughing. The light on screen moves from blue (watching) to yellow (detected, but not sure yet) to red (firing). Notice that a single brief cough doesn’t fire right away, then ask: how does it know which one is real?

A raw verdict can’t be trusted whole — a model can always flicker past the threshold for a moment. If you fired on every frame where label == "cough", you’d get so many false alerts that people would stop paying attention — the same problem as switch bounce on a button. So we build a four-stage pipeline: (1) filter: hit = r['label'] == TARGET_CLASS and r['conf'] >= edge_ai.CONF_FLOOR, cutting out unsure answers (2) debounce: streak += 1 on a hit, streak = 0 the instant it drops — it must reach NEED_HITS to be ready, so a single spike can’t get through because it doesn’t hold (3) cooldown: time.ticks_diff(now, last_fire) >= COOLDOWN_MS, guarding against rapid re-firing while an event stays active a long time — always use ticks_diff, since the ms counter can wrap around (4) action, inside a separate fire_action(): a coloured card on screen standing in for an RGB light (blue, yellow, red), ui.tone(note, wave, velocity, dur_ms) or ui.sfx(...) wrapped in hasattr, and a log through lcd.console with the time and confidence.

An extra layer of protection is EMA, $y_t = \alpha x_t + (1-\alpha) y_{t-1}$, at $\alpha = 0.35$. If $y_{t-1} = 0.20$ and a spike $x_t = 0.90$ appears, you get $y_t = 0.445$ — still short of 0.50. It takes several genuinely high frames in a row to climb over the line, keeping just one value of state. Debounce counts frames, while smoothing flattens the value’s size — used together, they’re even tighter. This can be written as maths: $c_t = c_{t-1} + 1$ on a hit, $c_t = 0$ on a miss, then fire when $[c_t \ge H] \wedge [c_{t-1} < H] \wedge [t - t_{last} \ge T_{cool}]$ — firing the moment readiness is first reached, once per event, exactly like an edge-triggered interrupt.

There are two ways to receive results. Polling means calling result() yourself every round, getting every frame, so you can count a streak. edge_ai.on_result(cb) has the firmware call us the instant the winning class changes, and repeats the same class about once a second — suiting logging a class change (remember the last class and skip the repeated rounds), but not suiting debounce. A job that needs to count frames or time uses polling; a job that just reacts to an event uses the callback, always removed with edge_ai.on_result(None) at the end.

19_motion_verdict_action.py checks the label, confidence, and inference time together before triggering a sound (threshold CONF = 0.70). 20_confirmed_alert.py confirms CONFIRM_N = 4 consecutive rounds before trusting a result, then waits ALERT_GAP_MS = 8000 before alerting again — the same pipeline, with a different set of numbers (both files were carried over from the author’s AIoT course).

File What this file teaches
examples/19_motion_verdict_action.py Gesture-triggered actions, and the cost of each decision
examples/20_confirmed_alert.py Treating a model as a source of values, then confirming before alerting

This lesson’s slides also reference files in another lesson:

The same questions are in quiz.yaml for automated checking.

  1. The cough class jumps above CONF_FLOOR for a single frame, then drops. Which stage stops it from firing? (single choice · objective 1)

    • a) The filter stage
    • b) The debounce stage, because streak never reaches NEED_HITS and resets to 0
    • c) The cooldown stage
    • d) The action stage
    Solution

    b — a spike high enough can pass the filter stage, but it doesn’t hold continuously, so streak never completes. Cooldown, on the other hand, exists to stop rapid re-firing while a real event stays active a long time.

  2. EMA at α = 0.35, an existing y = 0.20, meets a new value x = 0.90. What’s the new y? (single choice · objective 2)

    • a) 0.90
    • b) 0.55
    • c) 0.445
    • d) 0.315
    Solution

    c — 0.35 × 0.90 + 0.65 × 0.20 = 0.315 + 0.130 = 0.445. The spike gets flattened enough that it still hasn’t crossed the 0.50 threshold.

  3. NEED_HITS = 3, and the cooldown has already passed. The frame sequence hit, hit, hit, hit, hit — how many times does it fire? (single choice · objective 3)

    • a) 0
    • b) 1 time, at the third frame
    • c) 3 times
    • d) 5 times
    Solution

    b — it fires the first time c_t reaches 3 (c_{t−1} = 2 < 3). By the next frame, c_{t−1} already meets it, so it doesn’t fire again — and cooldown starts counting fresh too.

  4. Why use time.ticks_diff(now, last_fire) instead of now − last_fire? (single choice · objective 3)

    • a) It’s faster
    • b) The ticks_ms counter can wrap around; ticks_diff handles the wrap so the difference stays correct
    • c) It gives a result in seconds
    • d) There’s no difference
    Solution

    b — once the counter fills up and wraps around, a plain subtraction gives a negative or wrong value. ticks_diff is designed exactly for this case.

  5. Which task best suits edge_ai.on_result(cb)? (single choice · objective 4)

    • a) Counting a per-frame streak for debounce
    • b) Logging when the winning class changes, remembering the last class to skip rounds where the firmware repeats it
    • c) Measuring latency every frame
    • d) EMA smoothing every frame
    Solution

    b — the callback arrives on a class change and repeats about once a second, not every frame. A job that needs to count frames should use polling instead.

  • Run 20_confirmed_alert.py on the emulator with the Motion model, pressing Shake briefly versus holding it, and note when the confirmed status label changes.
  • Continue the EMA calculation from the example for three more frames, with x = 0.90 held steady, and see which frame crosses 0.50.
  • Write a ten-frame hit/miss sequence with both a single spike and a real event, then mark the frame that fires when NEED_HITS = 3.

In lesson 6.4, we’ll fill in the four pipeline stages in s16_action_pipeline.py, then tune NEED_HITS and COOLDOWN_MS until it genuinely guards against false positives.

Next lesson: lesson 6.4 — Hands-on: an action pipeline that guards against false positives

  • For what kind of work is missing a real event (a false negative) more costly than a false alert, and which way would you tune the pipeline?
  • If you used on_result to do debounce instead of polling, what problem would that cause?

Review questions

Answer on your own first, then open the answer.

  1. The cough class jumps above CONF_FLOOR for a single frame and drops. Which stage stops the firing? (Objective 1)

    1. ด่านกรอง
    2. ด่าน debounce เพราะ streak ไม่ถึง NEED_HITS แล้วรีเซ็ตเป็น 0
    3. ด่าน cooldown
    4. ด่าน action
    Show answer

    Answer: B. ด่าน debounce เพราะ streak ไม่ถึง NEED_HITS แล้วรีเซ็ตเป็น 0

    ยอดแหลมที่สูงพอผ่านด่านกรองได้ แต่ไม่ค้างต่อเนื่อง streak จึงไม่ครบ ส่วน cooldown มีไว้กันการยิงรัวตอนเหตุการณ์จริงค้างยาว

  2. An EMA with α = 0.35 and previous y = 0.20 meets a new x = 0.90. What is the new y? (Objective 2)

    1. 0.90
    2. 0.55
    3. 0.445
    4. 0.315
    Show answer

    Answer: C. 0.445

    0.35 × 0.90 + 0.65 × 0.20 = 0.315 + 0.130 = 0.445 ยอดแหลมถูกกดจนยังไม่ข้ามเกณฑ์ 0.50

  3. With NEED_HITS = 3 and the cooldown elapsed, how many times does hit, hit, hit, hit, hit fire? (Objective 3)

    1. 0
    2. 1 ครั้ง ที่เฟรมที่สาม
    3. 3 ครั้ง
    4. 5 ครั้ง
    Show answer

    Answer: B. 1 ครั้ง ที่เฟรมที่สาม

    ยิงเมื่อ c_t ถึง 3 เป็นครั้งแรก (c_{t−1} = 2 < 3) เฟรมถัดไป c_{t−1} ครบแล้วจึงไม่ยิงซ้ำ และ cooldown เริ่มนับใหม่ด้วย

  4. Why use time.ticks_diff(now, last_fire) instead of now − last_fire? (Objective 3)

    1. เร็วกว่า
    2. ตัวนับ ticks_ms วนกลับได้ ticks_diff จัดการการวนกลับให้ผลต่างยังถูก
    3. ได้หน่วยวินาที
    4. ไม่มีความต่าง
    Show answer

    Answer: B. ตัวนับ ticks_ms วนกลับได้ ticks_diff จัดการการวนกลับให้ผลต่างยังถูก

    เมื่อตัวนับเต็มแล้ววนกลับ การลบตรง ๆ จะได้ค่าติดลบหรือผิด ticks_diff ออกแบบมาเพื่อกรณีนี้

  5. Which task suits edge_ai.on_result(cb) best? (Objective 4)

    1. นับ streak ต่อเฟรมเพื่อ debounce
    2. log เมื่อคลาสที่ชนะเปลี่ยน โดยจำคลาสล่าสุดไว้เพื่อข้ามรอบที่เฟิร์มแวร์ทวนคลาสเดิม
    3. วัด latency ทุกเฟรม
    4. smoothing ด้วย EMA ทุกเฟรม
    Show answer

    Answer: B. log เมื่อคลาสที่ชนะเปลี่ยน โดยจำคลาสล่าสุดไว้เพื่อข้ามรอบที่เฟิร์มแวร์ทวนคลาสเดิม

    callback มาตอนคลาสเปลี่ยนและทวนราววินาทีละครั้ง ไม่ได้มาทุกเฟรม งานที่ต้องนับเฟรมจึงควรใช้ poll

Cite this lesson

If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.

"The action pipeline: CONF_FLOOR, debounce, cooldown and on_result" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0

Thai attribution: "ท่อสั่งการ: CONF_FLOOR, debounce, cooldown และ on_result" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/edge-ai-developer/m06-apps/l03-action-pipeline/

Full guide: how to cite TESA

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA