SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
J3 — The auto-push task and the sensor hub
Variant
mtb-mpy and mtb-only

sensor_auto_task.c ships as source in both packages and is created from main() in both. The sensors.auto* calls that steer it from Python are mtb-mpy only; the C control API is the same on both sides and is what those calls invoke.

Learning goal

After this chapter you can explain what puts a live number on the CM55 screen when nobody has called anything: one CM33_NS task, a rate, a bitmask, and one IPC hop into ipc_sensorhub. You will be able to change the rate for a DEEPCRAFT model without breaking the dashboard, say which sensors this task actually pushes on this board (it is fewer than the mask suggests), and — on mtb-only — know why deleting this one call takes every CM55 page down with it.

The real firmware sequence

One creation call, three jobs. sensor_auto_task_create() (sensor_auto_task.c:1513) is called from proj_cm33_ns/main.c:318, on both variants, after the storage and config branch. It creates the WiFi request queue, then — and this is the part with consequences far outside sensors — it calls cm33_ipc_communication_setup() if nobody has yet (sensor_auto_task.c:1523-1527), then creates the WiFi IPC worker and finally the push task itself.

The loop. The task wakes every s_interval_ms, default 100 ms — 10 Hz (sensor_auto_task.c:145) — and pushes only what the enable mask allows (:1420-1450). It is not a flat sweep; it has two tiers and a switch:

  • Fast tier, every cycle: BMI270.
  • Slow tier, about every 500 ms: DPS368 and SHT40.
  • The fast switch: when the interval drops below SENSOR_AUTO_FAST_THRESHOLD_MS = 50 ms (sensor_auto_task.c:153), the loop reads the accelerometer alone and skips every other sensor, because otherwise the period is not achievable.

At the end of each cycle the push counter increments and the task sleeps s_interval_ms (:1487-1490).

The mask has bits this board does not use. sensors.auto(name, bool) accepts six names, and sensor_auto_task.h:18-24 defines six bits. But the CapSense and pot pushes are compiled behind #if BSP_HAS_CAPSENSE && !BSP_HAS_QWA309_BASEBOARD and #if BSP_HAS_POTENTIOMETER && !BSP_HAS_QWA309_BASEBOARD (sensor_auto_task.c:1433-1439), and this board sets BSP_HAS_QWA309_BASEBOARD=1. On this board those two bits are accepted, stored and reported by auto_status(), and read by nothing. That is not a bug: on a QWA309 board CapSense and the pots are on the CM55 side, and CM55 feeds them into the hub itself — see Where the data lands: ipc_sensorhub.

The rate is clamped in two places, to the same range. sensor_auto_set_rate() clamps 20..5000 ms (sensor_auto_task.c:1605-1609) and sensors.auto_rate() clamps to the same range before calling it (modsensors.c:1236-1240). The floor is not arbitrary: the binding's own comment says 20 ms is 50 Hz, the rate DEEPCRAFT motion models are trained at, "so MicroPython must be able to ask for it too".

The Edge AI engine asks for the same thing over IPC. IPC_CMD_SENSOR_AUTO_CTRL op 2 carries a 16-bit interval and is sent by the CM55 Edge AI engine (sensor_auto_task.c:308-330). Setting the rate also un-pauses and resumes the task in the same message — deliberately, so a model start needs one message rather than two that would collide on the shared IPC buffer.

Where the data lands: ipc_sensorhub

The push side is this task; the receive side is ipc_sensorhub, in libbento_ipc.a, documented at Sensor hub. Do not re-derive it here — three things about the seam matter for this chapter:

  • ipc_sensorhub_init() must run after cm55_ipc_communication_setup() on the CM55 side, because it registers a pipe callback.
  • ipc_sensorhub_snapshot() clears the changed flags as it reads. Exactly one consumer per tick, or the second consumer sees "nothing changed". This is Appendix X #9 — Appendix X — Traps and anti-patterns.
  • The feed API — ipc_sensorhub_feed_bmi270, _feed_bmm350, _feed_capsense, _feed_pot — lets CM55 inject readings it took itself, with no IPC hop at all. That is the path cm55_sensor_poll uses for CapSense and the pots on this board, and it is why those two mask bits are dead on the CM33 side.

Step by step

Step 1 — Confirm the task is running (both variants)

On mtb-mpy, from the REPL:

Example (authored — no shipped call site)
import sensors
print(sensors.auto_status())

On mtb-only, from your own CM33_NS code:

Example (authored — no shipped call site)
#include "sensor_auto_task.h"
bool up = sensor_auto_is_running();
uint32_t rate = sensor_auto_get_rate();
uint32_t mask = sensor_auto_get_mask();
uint32_t count = sensor_auto_get_push_count();
What you should observe
running true, rate_ms 100 on a board nobody has re-rated, and a push_count that increases about ten times a second. The dict shape is running, rate_ms, push_count, mask (modsensors.c:1246). Nothing prints on either variant — this task is silent by design; the only lines it can emit are the two creation failures at sensor_auto_task.c:1519 and :1545. On screen, the Dashboard values move on their own: that is this task, and it is the one observable that needs no instrument at all.

Step 2 — Re-rate it for a 50 Hz model, then put it back

Example (authored — no shipped call site)
import sensors
sensors.auto_rate(20) # 50 Hz — the DEEPCRAFT motion training rate
print(sensors.auto_status()['rate_ms'])
sensors.auto_rate(100) # back to the 10 Hz dashboard default
What you should observe
20. And, while it is at 20 ms, the environment values on screen stop updating — pressure, humidity and the magnetometer all sit still. That is the fast switch at sensor_auto_task.c:1421 doing exactly what it says: below 50 ms the loop reads the accelerometer alone. This surprises people who expect "faster" to mean "faster for everything". It means the opposite for five of the six sensors, and it is the right trade, because a 20 ms period cannot be held while also servicing a 500 ms-class barometer read.

Ask for something outside the range and nothing raises: auto_rate(1) is clamped to 20 and auto_rate(99999) to 5000, in the binding and again in the C setter.

Step 3 — Turn one sensor off

Example (authored — no shipped call site)
import sensors
sensors.auto('sht40', False) # stop pushing humidity
print(hex(sensors.auto_status()['mask']))
sensors.auto('sht40', True)
What you should observe
The mask loses bit 2 (SENSOR_AUTO_SHT40, sensor_auto_task.h:20) and the humidity value on screen freezes at its last pushed reading — it does not blank and does not show an error, because the hub keeps the last value it was given. An unknown name raises ValueError (modsensors.c:1224).
Warning
Do this with 'capsense' or 'pot' on this board and nothing observable happens. The mask bit changes and auto_status() reports it, but no code reads it here — those two are fed from CM55. See Where the data lands: ipc_sensorhub.

Step 4 — Stop and start the whole task

Example (authored — no shipped call site)
import sensors
sensors.auto(False) # suspend
print(sensors.auto(), sensors.auto_status()['running'])
sensors.auto(True) # resume
What you should observe
Every live value on the CM55 screen freezes together, and resumes together. sensor_auto_stop() suspends the task outright rather than just setting a flag (sensor_auto_task.c:1591-1599) — the comment explains why: a flag alone would leave one more IPC burst in flight while the UI is probing CM55 after a soft-reset.

Step 5 — mtb-only: the call that is not about sensors

Read proj_cm33_ns/main.c:271-289. In the mtb-only branch, three jobs the MicroPython task used to do at boot have to happen in main() instead, and the third one is this:

sensor_auto_task_create() was the ONLY boot-path caller of cm33_ipc_communication_setup(); the two handler inits below RegisterCallback on that pipe and do not set it up themselves. Without this the link is clean, the boot is quiet, and every CM55 page that talks to CM33 is dead.

What you should observe
On a correct mtb-only boot: the heartbeat line every 10 s, and CM55 pages that answer. If you remove or reorder sensor_auto_task_create(), the build still links, the boot still prints its heartbeat, and every cross-core page goes quiet with no error anywhere. That failure mode — clean link, quiet boot, dead UI — is the reason variants/mtb-only.mk:15-17 records the ownership in writing. See also B1 — CM33_NS boot walk-through for the full boot order and B3 — The IPC backbone: setup, deferred binding, snapshots for the pipe itself.

Traps

Warning
Appendix X #9 — one snapshot consumer per tick. ipc_sensorhub_snapshot() clears the changed flags as it reads. A second consumer in the same tick sees an unchanged hub. Take one snapshot and fan it out.
Below 50 ms you are reading one sensor, not six. The fast switch is silent. If your environment readings "stopped working" after a rate change, this is why.
Two of the six mask bits do nothing on this board. capsense and pot are compiled out of the push loop when BSP_HAS_QWA309_BASEBOARD=1. Toggling them changes the reported mask and nothing else.
Do not call sensor_auto_task_create() twice expecting a restart. It returns immediately when the handle exists (sensor_auto_task.c:1514). Use sensor_auto_start() / sensor_auto_stop().
Never delete sensor_auto_task_create() from an mtb-only main() because "this board has no sensors to push". It owns the IPC pipe setup. See Step 5.

Variant box

mtb-mpy mtb-only
Task creation main.c:318 main.c:318 — same line, same call
IPC pipe setup done by mpy_main's path or by this task, whichever is first done by main() itself, and again guarded inside this task
Control surface sensors.auto(), auto_rate(), auto_status() sensor_auto_start/stop/set_rate/set_mask/enable/disable, sensor_auto_get_*
Rate clamp 20..5000 ms, applied twice 20..5000 ms, applied once
Observable auto_status() plus live values on screen live values on screen; [HB] proves the scheduler, not this task