Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ test:
- . venv/bin/activate
- pip install -q ${PIP_DEPS}
script:
# Static guard: SDK code must log through the facade (common/log.h
# log_tag_*), never call log_emit/printf directly -- a raw call site
# bypasses the compile-time ceiling and reappears in every build.
# Allowed: the facade + its sink (common/log.{h,c}) and test harnesses;
# examples and third_party are out of scope. The word boundary is
# hand-rolled as (^|[^A-Za-z0-9_]) so fprintf/snprintf-style libc calls
# are not flagged and the CI image's git needs no PCRE.
- |
LEAKS=$(git grep -nE '(^|[^A-Za-z0-9_])(log_emit|printf)[ ]*\(' -- modules common pal \
':!common/log.h' ':!common/log.c' ':(exclude,glob)**/test/**' || true)
if [ -n "$LEAKS" ]; then
echo "Raw log_emit/printf call sites outside the log facade:"
echo "$LEAKS"
echo "Route them through the log_tag_* macros in common/log.h instead."
exit 1
fi
# cd into the build dir rather than using `--test-dir` (the CI image's
# ctest does not honour that flag and would silently find no tests).
- cd ${CMAKE_BUILD_DIR}
Expand All @@ -71,14 +87,15 @@ test:memory-leak:
VG=modules/iot-client/test/run_valgrind_check.sh
chmod +x "$VG"
# Leak-check the pure (no-subprocess) binaries, grouped by module:
# rtc-tcp-client : tai_unit_tests, tai_integration_tests
# rtc-tcp-client : tai_unit_tests, tai_log_level_zero_tests,
# tai_log_module_zero_tests, tai_integration_tests
# iot-client : iot_cipher_test
# tuya-ble : tuya_ble_test
# rtc-client is a prebuilt closed lib (headers + libstm.a) with no offline
# test, so there is nothing to leak-check for it here. The mock-driven iot
# tests are covered by the `test` job; under valgrind their Python
# handshakes would time out.
for t in tai_unit_tests tai_integration_tests iot_cipher_test tuya_ble_test; do
for t in tai_unit_tests tai_log_level_zero_tests tai_log_module_zero_tests tai_integration_tests iot_cipher_test tuya_ble_test; do
echo "==================== valgrind: ${t} ===================="
"$VG" "./${CMAKE_BUILD_DIR}/${t}" 2>&1 | tee "valgrind-${t}.log"
rc=${PIPESTATUS[0]}
Expand Down
36 changes: 26 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,12 @@ ctest --test-dir build --output-on-failure --no-tests=error --timeout 180 # ne
omission is invisible until someone flashes a board. They diverge on purpose — `pal_posix.c`
and `iot_pal_defaults.c` host-only (each IDF app defines its own `get_default_pal()`),
`pal_freertos.c` IDF-only — so never blind-sync them.
5. **Module code goes through the PAL**: `pal->malloc`/`pal->free` for memory, `log_emit` for
output (via each module's prefixed `log_info`/`log_warn`/`log_error`). A direct `malloc` or
`printf` is a porting bug even where it links on the host. The one deliberate gap: `pal_t` has
5. **Module code goes through the PAL**: `pal->malloc`/`pal->free` for memory, the `log_tag_*`
macros in `common/log.h` for output (via each module's own family: `IOT_LOG*`,
`TAI_LOG*`, `TUYA_BLE_HAL_LOG*`). A direct `malloc` or
`printf` is a porting bug even where it links on the host -- and the `test` CI job greps for
raw `log_emit(`/`printf(` call sites outside `common/log.{h,c}` and the test trees, so one
fails the pipeline. The one deliberate gap: `pal_t` has
only a monotonic `time_ms`, so ATOP signing reads libc `time(NULL)` — a port needs a real-time
clock the C library can see, or every signed request carries a `t` the cloud rejects.
6. **CHANGELOG entries are terse, and carry a PR number.** One line per change —
Expand Down Expand Up @@ -138,7 +141,7 @@ ordinary changes.

- **`schema` and `dp_state` are one artefact; persist and restore them together.** A NULL, empty,
`"[]"` or unparseable schema is not an error anywhere: `iot_dp_rebuild()` installs *loose mode*
with one `log_info` line and `iot_client_init()` still returns a healthy client. In loose mode
with one `IOT_LOGI` line and `iot_client_init()` still returns a healthy client. In loose mode
cloud DP-sets are never dispatched, every `iot_dp_get`/`set` returns `OPRT_DP_INVALID_ID`,
`iot_dp_restore_json()` discards the whole snapshot and still returns `OPRT_OK`, and
`iot_dp_dump_json()` returns `{"dps":{}}` — which an app that periodically persists writes over
Expand Down Expand Up @@ -169,7 +172,7 @@ ordinary changes.
- **A protocol-5 downlink is always "consumed", even when nothing was applied.**
`iot_dp_dispatch_downlink()` returns consumed for every protocol-5 envelope — non-object `dps`,
every DP rejected, or the callback snapshot's malloc failing — with per-DP rejections at
`log_warn` only. No DP callback, no `message_callback`, no error return, and the cloud already
`IOT_LOGW` only. No DP callback, no `message_callback`, no error return, and the cloud already
has its QoS1 ack. If the app needs to see rejected DP-sets, add an explicit path.

### iot-client — credentials, config, transport
Expand Down Expand Up @@ -293,14 +296,27 @@ ordinary changes.
`paths-ignore` excludes docs-only pushes from all C jobs, and `deploy-docs.yml` triggers on
`main`, which does not exist here (only its `workflow_dispatch` fires it). GitLab's `pages` job
is the only thing that actually catches it, so build the site locally after touching it.
- **`-DLOG_LEVEL=...` does nothing.** `common/log.h` advertises it as a compile-time ceiling but
no file references it; `log_emit()` filters on a runtime global, after the varargs have been
evaluated. The only working compile-time gate is `TAI_LOG_LEVEL`, and it covers rtc-tcp-client
only.
- **`AGENTIC_KIT_LOG_LEVEL` is the SDK-wide log gate, compile-time only.** Every SDK log
macro (log_tag_* in `common/log.h`; iot-client's IOT_LOG*, TAI_LOG*, TUYA_BLE_HAL_LOG*
re-tagged on top) expands to nothing above it. Optional per-module ceilings
(`AGENTIC_KIT_{IOT,TAI,TUYA_BLE}_LOG_LEVEL`, defaults = the global one, in each module's
config file) lower a single module further -- lower-only: the facade gate still applies, so
they can never resurrect what the global ceiling compiled out, and a value above it is a no-op
(clamped once in each module's config file, so block-level gates see the effective ceiling too).
There is no runtime level: below the ceiling a
line emits unconditionally (`log_set_level()`/`log_get_level()` and the tai_set_log_level()
wrappers are gone, and so is the runtime handler -- `log_set_handler()` no longer exists).
The destination is a compile-time fact too: define `AGENTIC_KIT_LOG` and every line dispatches
into your own macro (a function target can reuse the default output via `log_emit_valist()`).
`log_emit()` remains the default sink, but module code never calls it directly -- a raw call
would bypass the ceiling. Even the media sampler in tai_pkt_log.c picks its INFO/DEBUG sink at
compile time -- a sampling build has exactly one level, and no runtime level comparison exists
anywhere in the SDK.
The old per-module `TAI_LOG_LEVEL` gate became the namespaced `AGENTIC_KIT_TAI_LOG_LEVEL`.
- **`mqtt_tls_config_t.verify_peer` is dead** — assigned in one place, read nowhere. Peer
verification is decided solely by whether `cacert` or `cert_bundle_attach` is non-NULL;
leaving both NULL is not "use the system trust store" (there is none on an embedded target),
it connects with verification disabled behind one `log_warn`.
it connects with verification disabled behind one `IOT_LOGW`.
- **`iot_client_process(client, timeout_ms)` ignores `timeout_ms`.** The real blocking budget is
the compile-time `AGENTIC_KIT_MQTT_RECV_TIMEOUT_MS` (1000 ms), and the CONNECT sets a 60 s keepalive. Do
not use the argument to pace the app loop.
Expand Down
23 changes: 15 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- docs-site — English edition of the full documentation site, published at `/en/` with Simplified Chinese retained at `/` (PR pending).
- docs-site — English edition of the full documentation site, published at `/en/` with Simplified Chinese retained at `/`.
- All 29 docs are mirrored under `docs-site/i18n/en/`, with a locale selector in both the landing-page navbar and the custom docs topbar, localized navbar/footer/sidebar catalogs, and English SVG schematics under `current/images/`.
- Heading anchors use explicit IDs shared across locales, and `npm run check:i18n` gates path, image, and anchor parity as part of `npm run build`.

Expand All @@ -23,10 +23,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- SDK-wide — every build-time knob is renamed with an `AGENTIC_KIT_` prefix and its default moves to its owning subsystem's config file; override per product with one `agentic_kit_config.h` on the include path, `-D<NAME>=<value>`, or `AGENTIC_KIT_USER_CONFIG` (#38).
- **BREAKING** `TAI_FRAG_BUF_SIZE` → `AGENTIC_KIT_TAI_FRAG_BUF_SIZE`, `RESPONSE_BUFFER_SIZE` → `AGENTIC_KIT_RESPONSE_BUFFER_SIZE`, and likewise the rest — old→new table in `docs-site/docs/guides/compile-time-knobs.md`.
- Knob defaults live at: the override pickup in `common/log.h`; PAL task sizing in `pal/pal_config_defaults.h`; per-module knobs in each module's include/ (`iot_client_config_defaults.h`, `tai_config_defaults.h`; tuya-ble has no knobs, so no defaults file until one appears). Non-knob module data (version strings, endpoints, log binding) lives in each module's src-side internal header (`src/iot_internal.h` restores and renames the former `iot_config_defaults.h`).
- Knob defaults live at: the override pickup in `common/log.h`; PAL task sizing in `pal/pal_config_defaults.h`; per-module knobs in each module's include/ (`iot_client_config_defaults.h`, `tai_config_defaults.h`, `tuya_ble_config_defaults.h`). Non-knob module data (version strings, endpoints, log binding) lives in each module's src-side internal header (`src/iot_internal.h` restores and renames the former `iot_config_defaults.h`).
- Removed the dead `IOT_DO_NOT_USE_CUSTOM_CONFIG` flag from the ESP-IDF component.
- SDK-wide — one compile-time log ceiling `AGENTIC_KIT_LOG_LEVEL` (0–4, default 4) gates every SDK log macro via the new `log_tag_*` facade in `common/log.h`; module code no longer calls `log_emit()` directly (#40).
- **BREAKING** `-DTAI_LOG_LEVEL=N` (rtc-tcp-client only) and the dead `-DLOG_LEVEL` both move to `-DAGENTIC_KIT_LOG_LEVEL=N`, which now silences the whole SDK at compile time. Lowering only rtc-tcp-client afterwards is the new `-DAGENTIC_KIT_TAI_LOG_LEVEL=N` (see below).
- Optional per-module log ceilings were added back on top: `AGENTIC_KIT_IOT_LOG_LEVEL`, `AGENTIC_KIT_TAI_LOG_LEVEL` and `AGENTIC_KIT_TUYA_BLE_LOG_LEVEL` all default to the SDK-wide ceiling and can only lower their one module below it (effective ceiling = the smaller of the two; values above the global ceiling are clamped to it once in each module's config file, so block-level gates see the effective ceiling too). They gate each module's vocabulary where it is defined, ride the same override pickup, and keep the compile-time-only property — below 3, the rtc-tcp-client packet-log formatter vanishes with its calls. tuya-ble gains its first knob and its `tuya_ble_config_defaults.h` this way.
- **BREAKING** the runtime log layer is removed: `log_set_level()`, `log_get_level()` and the `tai_set_log_level()`/`tai_get_log_level()` wrappers are gone, and `log_emit()` no longer filters — the ceiling is the only level gate, so what compiles in is what emits. Migrate runtime `log_set_level(N)` calls to a lower ceiling at build time, or to level filtering inside your `AGENTIC_KIT_LOG` target.
- **BREAKING** the runtime handler is removed too: `log_set_handler()`, `log_fn_t` and the public `log_default_handler()` are gone — the destination is a compile-time fact, with no mutable log state left in the SDK. Make the old handler function the `AGENTIC_KIT_LOG` target (it now receives the level and the bare tag directly), and use the new `log_emit_valist()` — the facade's `va_list` entry, the `esp_log_writev` role — to reuse the default stderr output from a function sink. The SDK's own tests migrated the same way: each test build remaps the dispatch into one sink whose capture/quiet/count are modes, not handler swaps.
- The dispatch itself is customer-remappable at compile time: defining `AGENTIC_KIT_LOG(level, tag, fmt, ...)` routes every SDK log line into your own macro — straight into `ESP_LOGx`-style systems with no `va_list` round trip (the tag arrives as its own token). The `AGENTIC_KIT_LOG_LEVEL` ceiling still applies, and since there is no runtime handler anymore, the remap is the one and only destination switch. The ESP-IDF pair-by-ble example now uses exactly this: `kit_opts/agentic_kit_config.h` maps the facade onto `ESP_LOG_LEVEL_LOCAL`, replacing its old runtime callback (each line also keeps its real module tag instead of one shared `"tuya_ble"` tag).
- rtc-tcp-client packet logging selects its INFO/DEBUG sink at compile time — sampling builds have exactly one level; flood mode needs `AGENTIC_KIT_TAI_LOG_MEDIA_SAMPLE_N=0` plus a DEBUG ceiling and vanishes otherwise — and its JSON formatter compiles out below INFO. A compile-matrix test builds 19 level × sampling combinations across both the SDK-wide and the rtc-tcp-client-only ceiling, including module-above-global clamp probes.
- tuya-ble — the SDK-internal log-facade binding, scan-token rotation and pending-credential
delivery are each defined once in `tuya_ble_internal.h` instead of being repeated per module (PR pending).
delivery are each defined once in `tuya_ble_internal.h` instead of being repeated per module.
- tuya-ble — bounded per-state Trsmitr reassembly, queued TX with backpressure, and configurable radio capability (#35).
- Ports drive `tuya_ble_prov_set_gatt_payload()`, `tuya_ble_prov_tx_ready()` and `tuya_ble_prov_tick()`; call `tuya_ble_prov_close()` on disconnect/reset.
- Recompile consumers for the public state/config layout changes; `comm_ability = 0` selects 2.4 GHz.
Expand All @@ -42,11 +49,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Docs — the pair-by-ble callback sample no longer claims credentials are never logged; the
demo's DEBUG protocol log prints the credential JSON on purpose, for device bring-up.
- Docs — the pair-by-ble stop section now also states the port's `nimble_port_deinit()` abort,
instead of implying every `tuya_ble_nimble_stop()` failure is retryable (PR pending).
- tuya-ble — preserve pending credentials under TX backpressure and route Trsmitr diagnostics through the log facade (PR pending).
- Examples — isolate BLE WiFi scan completions across cancellation, join workers before teardown, and restart STA before connecting (PR pending).
- tuya-ble — enforce Pairing/KEY_12 credential authorization, reject replayed Frames, and accept the app's extra CBC padding block (PR pending).
- tuya-ble — reset Pairing state on device-info re-query and defer credential delivery until queued acknowledgements are accepted by the port (PR pending).
instead of implying every `tuya_ble_nimble_stop()` failure is retryable (#35).
- tuya-ble — preserve pending credentials under TX backpressure and route Trsmitr diagnostics through the log facade (#35).
- Examples — isolate BLE WiFi scan completions across cancellation, join workers before teardown, and restart STA before connecting (#35).
- tuya-ble — enforce Pairing/KEY_12 credential authorization, reject replayed Frames, and accept the app's extra CBC padding block (#35).
- tuya-ble — reset Pairing state on device-info re-query and defer credential delivery until queued acknowledgements are accepted by the port (#35).
- iot-client — US-East (`UEAZ`) fell back to an ATOP host that does not resolve(#31).
`IOT_UEAZ_HOST` is now `a1-ueaz.tuyaus.com`.

Expand Down
Loading
Loading