SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
Appendix X — Traps and anti-patterns
Variant
mtb-mpy and mtb-only

A compiled register. Every entry cites the line that proves it. Entries are numbered as in the design of record so chapters can refer to them by number.

1. CM55 printf is a no-op by default

CM55 has no console: CM33_NS owns the UART, and newlib stdio on CM55 would take a mutex that is never initialised and abort the core. proj_cm55/bento_cm55_stdio.c, which is open source, therefore defines printf and puts as weak no-ops. A printf in your own CM55 code prints nothing until you define your own printf (strong wins the link) or route the message over IPC to the CM33_NS console. libbento_edge_ai.a defines neither symbol (neither is in lib/edge_ai/api.txt).

2. Never send a raw IPC_CMD_TOUCH_RESUME

CM55 treats it as an unconditional touch_disabled = false, cancelling another task's touch hold and producing OPTIGA_COMMS_ERROR (0x0102) on the shared bus (ipc_hsm_handler.c:1393-1405). Use the counted optiga_manager_touch_hold()/_release() pair.

3. optiga_chip_enter() is not an init check

It deliberately returns true when the manager was never initialised (declared in tesaiot_hsm_api.h). optiga_manager_lock() is the "is the manager up?" test; getting this backwards is the documented cause of several ungated-access defects.

4. Never ai_engine_resume_sensor() right after set_sensor_rate()

Both requests are asynchronous, and a resume issued while a rate request is still in flight can cancel it: CM33 then pauses the sensor push instead of changing its rate. set_sensor_rate() already resumes the push. resume_sensor has no caller in shipped firmware.

5. Never gate a model fallback on ai_engine_active()

active() reports the model that has finished loading, so it lags the request by a whole cold-init. The recorded symptom: select Radar, press Load, and the Motion model runs. Guard on ai_engine_requested().

6. ai_engine_snapshot() in a set loses detections

It returns the most recent verdict from any model. In a set, a cough detected at 0.94 is followed by three "unlabelled" verdicts from the other members before a reader sees it — the detection is not late, it is gone. Use snapshot_model() per member.

7. Blind ipc_lcd_toggle_panel() inverts state

There is no idempotent set form; always guard with ipc_lcd_is_panel_visible() (ipc_core row 2, page_playground.c:167-188). Console and widgets are mutually exclusive.

8. Forgetting set_container(NULL) on page destroy is a use-after-free

lv_screen_load_anim(auto_del) frees the page's objects; a later IPC handler writes into them (ipc_core row 3, page_playground.c:342-349). NULL both containers on destroy, in the same order as the bind.

9. ipc_sensorhub_snapshot clears the changed flags

"Safe to call from any task context" but "Clears the 'changed' flags after reading" (ipc_sensorhub.h:66-67) — exactly one consumer per tick. The UI takes one snapshot at 33 ms and fans out.

10. UI list index is not the OPTIGA slot index

Resolve SSID → slot with wifi_saved_find() before any erase (wifi_connect_native.c:118-134); < 0 on miss.

11. The mtb-mpy credential flush never runs under a looping /main.py

wifi_creds_flush_if_dirty is called only at mpy_main.c:709 and :686 (REPL idle / exit). A program that never returns to the REPL never persists the credential; mtb-only writes immediately (sensor_auto_task.c:784-789 vs :828-854).

12. Retained Protected Update bundles replay

A retained bundle arrives on every connect; observed three times on 2026-08-07 silently redoing the previous run. trustm_current_correlation_id() == NULL means nothing is outstanding — discard (tesaiot_pu_ingest.c:752-768). At weak sites check the function pointer, then the string.

13. Unbounded I2C timeouts on the shared display bus starve CM55

The GFX task runs at MAX−1; a wedged clock-stretching device with a block-forever PDL timeout starved every lower-priority CM55 task. Use bounded timeouts (2 ms) on disp_touch_i2c_controller_context (cm55_sensor_poll.c:49-53).

14. PAGE_ID ordinals are ABI baked into libbento_ipc.a

page_id_ordinal_assert.c:22-25; lib/ipc_core/PROVENANCE.txt records PAGE_ID_PLAYGROUND == 7. A mismatch links cleanly and compares the wrong page. Add at the end; never renumber, never re-guard.

15. cfg.port is stored but unused

The MQTT port derives from tls_mode (mqtt_client_config.c:105-118). Editing port in /.tesaiot_config changes nothing.

16. Debugger attach parks a running mtb-only CM33 in a boot-ROM loop

main.c:278-287. The heartbeat exists because the variant has no instrument "that is not also the murder weapon". Flash, then power-cycle; do not attach to a running board.

18. Two unlocked credential writers can race the LFS file

modwifi.c:261-285 and wifi_init.c:261-302 read-modify-write the file directly without wifi_creds_lock and bypass g_boot_wifi_creds; there is no file-level lock outside mtb-only's bs_lock() (J8 thin-evidence #6).

19. Weak-consumed HSM symbols require pointer NULL checks

Six of the 18 tesaiot_hsm functions link only under ENABLE_OPTIGA_CLM=1 and are consumed as weak symbols: publish_csr, tesaiot_publish_protected_update, trustm_reset_state, trustm_current_correlation_id, trustm_requested_target_oid, trustm_requested_anchor_oid (A.5 preamble; refusal string at ipc_hsm_handler.c:2158-2161).

20. bento_buddy_auto_start_install is superseded

The legacy task skipped the WL_REG_ON / WCM init step that CYW55513 needs (main.c:344-347). No caller anywhere; use install_chip_power_then_ble().

21. Power-cycle after every flash

A debugger reset leaves the display dark, which looks exactly like a failed flash (shipped package READMEs: dist mtb-mpy :51-52, mtb-only :54; in-zip README :47-49). Taught in A1/A2 at the first checkpoint. Dark screen after flash = reset, not failure.

22. Cold-boot backlight (mtb-only)

The backlight can want a second unplug-replug on cold boot (dist mtb-only README :55). A dark screen after a power-cycle is not proof of a bad image.

23. A trailing space in bsp_features.mk silently drops the feature

Values must be a bare 1 or 0. The consuming Makefiles compare with ifeq ($(VAR),1), which is whitespace-sensitive, so BSP_HAS_CAPSENSE=1 (or an inline comment after the value) evaluates false and the feature's INCLUDES/CFLAGS vanish with no error anywhere — the build succeeds and the sensor is simply not there. The file states the rule itself (bsp_features.mk:28-30, restated at :55-57). Taught in The BSP flags, as this board sets them.

25. 0x08060009 is not a diagnosis — it overwrites the real error

CY_RSLT_MODULE_MQTT_CONNECT_FAIL is assigned when the retries run out (cy_mqtt_api.c:2436), overwriting whatever result already held. A TLS failure and an authentication rejection therefore print the identical code. On 2026-09-11 three defects at three different layers all printed it, and it never changed once as each was fixed.

The signal that does work is the [TLS-DIAG] err= line at cy_tls.c:1860. It sits inside the mbedtls_ssl_handshake() loop, so it fires on every handshake failure — which means its absence is proof that TLS succeeded. Do not read the other [TLS-DIAG] lines (optiga_key, heap) that way: they print before the handshake and say nothing about it.

26. A successful enrolment changes which certificate the board presents

mqtt_mtls_setup.c:232 switches to 0xE0E1/0xE0F1 the moment verify_cert_key_pair() is true, so the certificate on the wire is not a configuration choice — it is decided at connect time by whether enrolment has succeeded.

The failure is inverted: a board that fails enrolment connects, and a board that succeeds is refused, until the broker carries CN=TESAIoT MCU CA in its trust bundle and its CN check compares against the device_id rather than the MQTT client id (the firmware deliberately sends the Trust M UID as client id). See D2 — Enrolment and Protected Update end to end.

27. Silent MQTTS with working HTTPS is a wedged chip, not a network fault

After several enrolment cycles the secure element can stop opening (optiga_util_open_application failed, status 0x0102). mTLS then cannot read the client certificate and MQTT never starts — tesaiot.connect() prints nothing at all, not even [MQTT] Start request received.

HTTPS keeps working throughout, because it authenticates with an API key and never touches the chip. That asymmetry makes it look like an MQTT or broker problem when it is neither. Check optiga.init() before reading any broker log, and note that reflashing does not clear it: a flash resets the MCU, not the secure element. Only a power cut does.

28. printf on CM33 and CM55 is newlib-nano — no llu, zu, jd, hhu

Both cores link with --specs=nano.specs. newlib-nano knows exactly one length modifier character from h, l, L (_WANT_IO_LONG_LONG is off), so llu prints the letters "lu" and consumes no argument. Every conversion after it reads the argument meant for the one before; a later s dereferences an integer and the core takes a BusFault in memchr ← _printf_i. zu, jd, td and hhu fail the same way, and -Wformat cannot catch any of them because they are valid C99. Print a 64-bit value as two 32-bit halves (0x%08lx%08lx) or convert it yourself (bento_time.c bt_i64_to_str); cast size_t to unsigned long and use lu.

29. OIDs 0xF1D5, 0xF1D6 and 0xF1D8–0xF1DB are not free — they hold the saved WiFi networks

The WiFi page keeps up to six saved networks in slots 5, 6, 8, 9, 10 and 11 (wifi_saved.h), one 106-byte record per OID, SSID and password together. The names TESAIOT_SLOT_WIFI_SSID and TESAIOT_SLOT_USER0..3 do not describe that. The store treats any object shorter than one record as an empty slot and fills empty slots first, so a short value an application keeps in one of these OIDs (an API key, say) is overwritten the next time a network is saved on the screen. 0xF1D2 holds the HSM page PIN hash; 0xF1D4 is reserved for Protected Update. Keep application data in 0xF1E0 or 0xF1E1 (1500 bytes each). The full table is in ipc_communication.h.

30. A CM33 flash write stops CM55 unless it goes through bento_storage

CM55 executes in place from the same NOR chip that holds the LittleFS volume. While that chip erases a 256 KB sector (typically ~0.8 s, at most 5.9 s) it answers no reads, and CM55's next instruction fetch never returns: the frame stops mid-draw, touch dies, the IPC pipe wedges. bento_storage asks CM55 to park in ITCM before every erase and program (the XIP guard, ipc_xip_guard.h) and refuses the write when CM55 does not answer. Never call mtb_serial_memory_erase/write or Cy_SMIF_Mem* directly while CM55 is running. In the mtb-mpy variant the MicroPython QSPI driver takes the same handshake (EXT_FLASH_XIP_GUARD=1). Batch high-rate data in RAM and write it in blocks: each write can pause the screen for up to one erase.

31. A non-secure HardFault ends silently in the secure core

The default SCB_AIRCR_BFHFNMINS_VAL 0 (the secure image's partition_ARMCM33.h) sends BusFault and HardFault to the secure side, which records the frame in cy_faultFrame and spins in Cy_SysLib_ProcessingFault without printing anything. The non-secure side just stops while CM55 keeps drawing. During development it can be set to 1 so the non-secure handlers see the fault (the handlers in ns_start_pse84.c now take the stack from EXC_RETURN), but that also hands faults raised in secure code, and NMI, to the non-secure side. Do not ship it.