SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
E2 — Parallel sets and result polling
Variant
mtb-mpy and mtb-only

MicroPython examples are mtb-mpy only; the C-side reduction and the on-screen observables apply to both variants. All C excerpts are lifted from archived sources (see the note in E1 — Select, confirm, start: REQUESTED versus ACTIVE).

Note
Credit: the Edge AI models are Infineon's, not TESAIoT's. The motion, audio and radar models this firmware ships (proj_cm55/modules/ai_models/model_*.c) are DEEPCRAFT™ Studio exports, copyright Imagimob AB, an Infineon Technologies company. The Siren, Cough and Factory Alarm models are DEEPCRAFT™ Ready Models by the same author, published by Infineon under the Imagimob AI Model Evaluation License Agreement, and are not redistributed here. TESAIoT trained none of them and owns none of them — what is ours is the engine around them. The shipped model sources carry a bare "All Rights Reserved" reservation and no grant of any kind, so they are credited here rather than licensed on. Our use is research and teaching, and not commercial; a reader who needs a grant must obtain it from Infineon and Imagimob. Train your own at https://www.infineon.com/design-resources/embedded-software/deepcraft-edge-ai-solutions/deepcraft-studio Full credit and the clause citations: THIRD_PARTY_NOTICES.md §2.2, §2.4 and §4.3.

Learning goal

Run several models at once as a set, read a verdict from the set without losing detections, and recognise the settle gate that withholds verdicts while the audio windows fill. The single most important sentence: ai_engine_snapshot() is not valid inside a parallel set.

Real firmware sequence

Set constants (ai_engine.h:97-113): MIC = 255, ROOM = 254, INTRUDER = 253, ALL = 252, AI_PARALLEL_FIRST = ALL. The legacy 13/14/15 aliases resolve to 253/254/255 (Chapter E1). An archive that predates ALL rejects 252 on hardware (ai_engine.h:109-113). Engine acceptance of any set index requires EDGE_AI_HAS_MIC (ai_engine.c:1850-1859).

Entering a set rebuilds membership on every entry (set_resolve(), ai_engine.c:1166-1189): explicit membership from set_define wins outright, otherwise the compiled name-match (set_wants(), ai_engine.c:1092-1127). Nothing is cached, so a model whose init() failed once is not exiled for the boot. Explicit-wins precedence is at ai_engine.c:1146-1149.

Custom membership — edge_ai.set_define(set, [i, j, ...]) (modedgeai.c:867-921): resolve the legacy index (:876), SET_BEGIN (:886), one SET_ADD per member paced at 5 ms (:895-902), SET_COMMIT (:904), 40 ms settle (:907), then a Q_SET read-back (:910) — the read-back is the only thing that catches a dropped ADD. CM55 applies the three-command build whole:

Origin
Lifted from deepcraft_task.c:727-749 (compiled into the prebuilt archive; not shipped as source).

The read-back that serves as the only acknowledgement:

Origin
Lifted from deepcraft_task.c:334-355 (compiled into the prebuilt archive; not shipped as source).

Result reduction inside a set — the loudest positive member, via ai_engine_set_members() + ai_engine_snapshot_model() + ai_result_top_positive():

Origin
Lifted from deepcraft_task.c:433-470 (compiled into the prebuilt archive; not shipped as source).

Scores are converted to 0..100 at deepcraft_task.c:478-483; class_count == 0 means "no verdict yet" and the CM33 side returns None (:487).

Sensor-rate side effect. dc_desired_sensor_rate() (deepcraft_task.c:259-269) walks the set and raises the accelerometer to 50 Hz if any member is AI_SENSOR_IMU — :250-254 records that Intruder Watch's Motion member was being fed at 10 Hz and its row never moved. ai_engine_model() returns NULL for a set pseudo-index, so branch on idx >= AI_PARALLEL_FIRST first:

Origin
Lifted from deepcraft_task.c:239-275 (compiled into the prebuilt archive; not shipped as source).

Step by step

Step 1 — Enumerate the sets the page offers (both variants)

On screen, open the model dropdown on the Edge AI page. Rows for sets are added only when the set has at least two members and a name:

Origin
Lifted from page_edge_ai.c:1033-1047 (compiled into the prebuilt archive; not shipped as source).

What you should observe. Set rows (Sound Watch, Room Presence, Intruder Watch, All Models) appear after the compiled single models. A set with fewer than two resolvable members is simply absent — that is a build-content fact, not a fault.

Step 2 — Select a set and watch the settle gate

>>> edge_ai.select(255) # MIC set; 15 is the legacy alias
True
>>> edge_ai.result()
>>> edge_ai.result()

What you should observe. For the first seconds result() returns None — class_count == 0 — while the audio windows fill. On screen the verdict label reads filling windows lu%% with the percentage climbing (page_edge_ai.c:698-699), and the badge shows CHIP_LOADING. The gate is ai_engine_mic_settling() / ai_engine_mic_settle_pct():

Origin
Lifted from page_edge_ai.c:685-708 (compiled into the prebuilt archive; not shipped as source).

Note the rule recorded at page_edge_ai.c:702-705: the stats line keeps updating during settle, otherwise an NPU stall inside the window is invisible. mic_settle_pct() is meaningful only inside a set; it returns 100 otherwise.

Step 3 — Poll results

>>> r = edge_ai.result()
>>> r

What you should observe. Once settled, a result whose fields come from the loudest positive member. Each member row on screen carries its own silence timer, derived from seq staleness (page_edge_ai.c:728-739):

Origin
Lifted from page_edge_ai.c:717-723 (compiled into the prebuilt archive; not shipped as source).

ai_engine_snapshot_model() returning false means "never published", which is not the same as "silent"; the index must be a real registry index, never a set pseudo-index.

Step 4 — Define your own membership

>>> edge_ai.set_define(253, [0, 2])
>>> edge_ai.select(253)
True

What you should observe. set_define returns after the Q_SET read-back confirms every member; a dropped ADD shows up as a shorter list, and the whole definition is refused if any index is invalid (all-or-nothing, ai_engine.c:1060-1064). Membership is 1..8 entries. set_define(252, ...) always fails — AI_PARALLEL_ALL has no slot (set_slot(), ai_engine.c:1040-1048).

Step 5 — Read active() once per render (C side)

Origin
Lifted from page_edge_ai.c:565-575 (compiled into the prebuilt archive; not shipped as source).

What you should observe. Nothing visible — this is the discipline that keeps the layout tag and the row count from disagreeing when the deepcraft task starts or stops the engine from another task mid-render.

Traps

Warning
The snapshot-in-set hazard. ai_engine_snapshot() is last-writer-wins over one slot. In a set, "a cough detected at 0.94 is overwritten by three 'unlabelled' … The detection is not late, it is gone" (ai_engine.c:1876-1885). Use ai_engine_snapshot_model() per member and reduce. The branch that decides between the two was == AI_PARALLEL_MIC until 2026-08-09, so Room Presence and Intruder Watch fell into exactly the slot the branch exists to avoid (deepcraft_task.c:448-450) — the correct test is the range [AI_PARALLEL_FIRST, AI_PARALLEL_MIC].
set_define is task context only. It takes taskENTER_CRITICAL around the publish (ai_engine.c:1059-1075); never call it from the pipe callback.
ai_engine_model_count() leads the rows. ai_engine_model(i) may still return NULL for a resolver hole — continue, do not abort:
Origin
Lifted from page_edge_ai.c:1019-1032 (compiled into the prebuilt archive; not shipped as source).

Variant box

mtb-mpy mtb-only
Select a set edge_ai.select(252..255) Dropdown row + Load
Define membership edge_ai.set_define() Only via your own CM55 code calling ai_engine_set_define() from a task; read back with ai_engine_set_members_defined()
Settle observable result() is None; screen filling windows lu%% Screen filling windows lu%%
Verdict edge_ai.result() dict Verdict label + per-row silence timers