Skip to content
Draft
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
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,16 @@ jobs:
run: pip install $PIP_DEPS

- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DPython3_EXECUTABLE=$(which python3)
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DPython3_EXECUTABLE=$(which python3) -DAGENTIC_KIT_ENABLE_LAN=ON

- name: Build
run: cmake --build build -j 4

- name: Build without LAN
run: |
cmake -S . -B build-no-lan -DCMAKE_BUILD_TYPE=RelWithDebInfo -DPython3_EXECUTABLE=$(which python3) -DAGENTIC_KIT_ENABLE_LAN=OFF -DAGENTIC_KIT_BUILD_TESTS=OFF
cmake --build build-no-lan --target tuya_iot_client tuya_ai_tcp -j 4

- name: FreeRTOS PAL header compatibility
run: cmake --build build --target check_freertos_pal

Expand Down Expand Up @@ -79,6 +84,7 @@ jobs:
run: >
cmake -S . -B build-asan
-DCMAKE_BUILD_TYPE=Debug
-DAGENTIC_KIT_ENABLE_LAN=ON
-DPython3_EXECUTABLE=$(which python3)
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -g -O1"
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
Expand Down Expand Up @@ -121,7 +127,7 @@ jobs:
# examples/posix fetches qrcodegen / quirc / stb via FetchContent at
# configure time, and links the prebuilt rtc-client lib (linux-gnu-amd64).
- name: Configure
run: cmake -S examples/posix -B build-examples -DPython3_EXECUTABLE=$(which python3)
run: cmake -S examples/posix -B build-examples -DPython3_EXECUTABLE=$(which python3) -DAGENTIC_KIT_ENABLE_LAN=ON

- name: Build
run: cmake --build build-examples -j 4
10 changes: 7 additions & 3 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ build:
- pip install -q ${PIP_DEPS}
script:
- cmake --version
- cmake -B ${CMAKE_BUILD_DIR} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DPython3_EXECUTABLE=$(which python3)
- cmake -B ${CMAKE_BUILD_DIR} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DPython3_EXECUTABLE=$(which python3) -DAGENTIC_KIT_ENABLE_LAN=ON
- cmake --build ${CMAKE_BUILD_DIR} -j$(nproc)
- cmake -S . -B build-no-lan -DCMAKE_BUILD_TYPE=RelWithDebInfo -DPython3_EXECUTABLE=$(which python3) -DAGENTIC_KIT_ENABLE_LAN=OFF -DAGENTIC_KIT_BUILD_TESTS=OFF
- cmake --build build-no-lan --target tuya_iot_client tuya_ai_tcp -j$(nproc)
artifacts:
paths:
- ${CMAKE_BUILD_DIR}/
Expand Down Expand Up @@ -90,17 +92,19 @@ 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:
# pal : pal_freertos_time_*_test, pal_posix_time_test
# common : json_lexical_test, bounded_string_test
# pal : pal_freertos_time_*_test, pal_posix_time_test, pal_net_test, pal_freertos_net_test
# rtc-tcp-client : tai_unit_tests, tai_log_level_zero_tests,
# tai_log_module_zero_tests, tai_integration_tests
# iot-client : iot_cipher_test, iot_ai_ctrl_test
# tuya-ble : tuya_ble_test
# biz-client : biz_ai_role_test (public ATOP boundary faked)
# tuya-lan : tuya_lan_protocol_test, tuya_lan_service_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 pal_freertos_time_16_1000_test pal_freertos_time_32_1000_test pal_freertos_time_32_100_test pal_freertos_time_32_128_test pal_freertos_time_32_2000_test pal_freertos_time_64_1000_test pal_posix_time_test biz_ai_role_test tai_unit_tests tai_log_level_zero_tests tai_log_module_zero_tests tai_integration_tests iot_cipher_test iot_ai_ctrl_test tuya_ble_test; do
for t in json_lexical_test bounded_string_test pal_freertos_time_16_1000_test pal_freertos_time_32_1000_test pal_freertos_time_32_100_test pal_freertos_time_32_128_test pal_freertos_time_32_2000_test pal_freertos_time_64_1000_test pal_posix_time_test pal_net_test pal_freertos_net_test biz_ai_role_test tai_unit_tests tai_log_level_zero_tests tai_log_module_zero_tests tai_integration_tests iot_cipher_test iot_ai_ctrl_test tuya_ble_test tuya_lan_protocol_test tuya_lan_service_test; do
echo "==================== valgrind: ${t} ===================="
"$VG" "./${CMAKE_BUILD_DIR}/${t}" 2>&1 | tee "valgrind-${t}.log"
rc=${PIPESTATUS[0]}
Expand Down
18 changes: 10 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# AGENTS.md

Instructions for AI coding agents working in this repository: a device-side C SDK for Tuya
hardware, spanning three bounded contexts — an IoT client (device ↔ cloud over MQTT/ATOP), an
RTC/TAI client (device ↔ AI Foundation, real-time audio over a custom binary framing), and BLE
provisioning — over a Platform Abstraction Layer, with mbedTLS / cJSON / coreMQTT / coreHTTP
vendored under `third_party/`. Sections follow the team outline.
hardware, spanning five bounded contexts — an IoT client (device ↔ cloud over MQTT/ATOP), an
RTC/TAI client (device ↔ AI Foundation, real-time audio over a custom binary framing), BLE
provisioning, Tuya LAN local control, and cloud business services — over a Platform Abstraction
Layer, with mbedTLS / cJSON / coreMQTT / coreHTTP vendored under `third_party/`. Contexts and
relationships are registered in `CONTEXT-MAP.md`. Sections follow the team outline.

## Commands

Expand Down Expand Up @@ -46,7 +47,8 @@ ctest --test-dir build --output-on-failure --no-tests=error --timeout 180 # ne
reset callback fires on that thread. rtc-tcp-client: `tai_connect()` spawns a worker via
`pal->thread_create`, and every receive callback fires there, concurrently with the app.
tuya-ble: no thread — it runs on whatever context the port's BLE stack calls `tuya_ble_recv()`
from. Never move work between these worlds.
from. tuya-lan: single-threaded and app-driven by `tuya_lan_process()`; all LAN calls and
callbacks stay on that owner thread. Never move work between these worlds.
- **The SDK does no background work.** It never auto-reconnects MQTT, and the DP layer never
publishes on its own — no report-on-connect, no cloud-query-triggered report
(`modules/iot-client/docs/adr/0001-dp-layer-never-initiates-uplink.md`). The app owns the
Expand Down Expand Up @@ -96,7 +98,7 @@ ctest --test-dir build --output-on-failure --no-tests=error --timeout 180 # ne
`pal_freertos.c` IDF-only — so never blind-sync them.
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
`TAI_LOG*`, `TUYA_BLE_HAL_LOG*`, `LAN_LOG*`, `BIZ_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. `pal_t.time_ms` supplies monotonic elapsed time;
Expand Down Expand Up @@ -307,9 +309,9 @@ ordinary changes.
`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.
- **`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*
macro (log_tag_* in `common/log.h`; iot-client's IOT_LOG*, TAI_LOG*, TUYA_BLE_HAL_LOG*, LAN_LOG*, BIZ_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
(`AGENTIC_KIT_{IOT,TAI,TUYA_BLE,LAN,BIZ}_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).
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- biz-client — `AGENTIC_KIT_BIZ_LOG_LEVEL` lowers business-service logs below the SDK-wide ceiling (#48).
- biz-client — typed AI role management, language/timbre queries, POSIX and ESP-IDF examples, and bilingual API documentation (#47).
- tuya-lan — application-driven LAN 3.5 discovery, authenticated Sessions and local DP control (#48).
- Opt in with CMake `AGENTIC_KIT_ENABLE_LAN=ON` or ESP-IDF `CONFIG_AGENTIC_KIT_ENABLE_LAN=y`; both default off.
- Server/datagram callbacks are optional members of `pal_t`; custom LAN ports must implement them. Rebuild consumers after the layout change and pass only `config.pal`.
- iot-client — `iot_client_apply_local_dps()` validates local DP controls, marks changes dirty and invokes existing callbacks (#48).

- iot-client — `iot_get_binding_status()` queries `tuya.device.info.sync` with saved credentials, without an IoT client (#44).
- `iot_client_get_binding_status()` reuses an existing client's endpoint and TLS settings.
- Examples — `unbind_demo` checks cloud binding status before client initialization/reconnect and after a removal notice (#44).
Expand All @@ -26,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- pal — socket handle allocation failures emit an explicit allocation diagnostic (#48).
- pal — FreeRTOS `time_ms` preserves tick wraps and converts non-integral tick periods correctly (#45).
- biz-client — role request timestamps and the clock gate read PAL UTC instead of libc time, so ports without a synchronized libc clock work (#45).

Expand Down
87 changes: 87 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ set(CMAKE_C_EXTENSIONS ON)

option(AGENTIC_KIT_BUILD_TESTS "Build tests" ON)
option(AGENTIC_KIT_BUILD_EXAMPLES "Build posix examples" OFF)
option(AGENTIC_KIT_ENABLE_LAN "Build Tuya LAN 3.5 local control" OFF)

include(CTest)
include(GNUInstallDirs)
Expand Down Expand Up @@ -142,12 +143,15 @@ add_library(agentic_kit_common STATIC
"${COMMON_DIR}/log.c"
"${COMMON_DIR}/tls.c"
"${COMMON_DIR}/rng.c"
"${COMMON_DIR}/json_lexical.c"
)
target_include_directories(agentic_kit_common PUBLIC
"${COMMON_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/pal" # tls.c / tls.h need pal.h
)
target_link_libraries(agentic_kit_common PUBLIC ${AGENTIC_KIT_MBEDTLS_TARGETS})
target_compile_definitions(agentic_kit_common PUBLIC
AGENTIC_KIT_ENABLE_LAN=$<BOOL:${AGENTIC_KIT_ENABLE_LAN}>)

# -----------------------------------------------------------------------------
# PAL (Platform Abstraction Layer)
Expand Down Expand Up @@ -285,10 +289,43 @@ target_link_libraries(tuya_ble PUBLIC
${AGENTIC_KIT_MBEDTLS_TARGETS}
)

# -----------------------------------------------------------------------------
# tuya-lan module (application-driven Tuya LAN 3.5 service)
# -----------------------------------------------------------------------------
if(AGENTIC_KIT_ENABLE_LAN)
set(TUYA_LAN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/tuya-lan")
add_library(tuya_lan STATIC
"${TUYA_LAN_DIR}/src/tuya_lan.c"
"${TUYA_LAN_DIR}/src/tuya_lan_protocol.c"
)
target_include_directories(tuya_lan PUBLIC "${TUYA_LAN_DIR}/include"
PRIVATE "${TUYA_LAN_DIR}/src" "${cjson_source_dir}")
target_link_libraries(tuya_lan PUBLIC agentic_kit_pal agentic_kit_common
${AGENTIC_KIT_CJSON_TARGET})
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(tuya_lan PRIVATE -Wall -Wextra -Werror)
endif()
endif()

# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------
if(AGENTIC_KIT_BUILD_TESTS AND AGENTIC_KIT_ENABLE_PROJECT_TESTS)
add_executable(json_lexical_test
"${COMMON_DIR}/json_lexical.c" "${COMMON_DIR}/test/test_json_lexical.c")
target_include_directories(json_lexical_test PRIVATE "${COMMON_DIR}")
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(json_lexical_test PRIVATE -Wall -Wextra -Werror)
endif()
add_test(NAME json_lexical_test COMMAND json_lexical_test)

add_executable(bounded_string_test "${COMMON_DIR}/test/test_bounded_string.c")
target_include_directories(bounded_string_test PRIVATE "${COMMON_DIR}")
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(bounded_string_test PRIVATE -Wall -Wextra -Werror)
endif()
add_test(NAME bounded_string_test COMMAND bounded_string_test)

# Fake only the public ATOP boundary, so service parsing/ownership can be
# checked without network credentials or a subprocess. Register in ctest.
add_executable(biz_ai_role_test ${BIZ_CLIENT_SOURCES}
Expand All @@ -304,6 +341,10 @@ if(AGENTIC_KIT_BUILD_TESTS AND AGENTIC_KIT_ENABLE_PROJECT_TESTS)
endif()
add_test(NAME biz_ai_role_test COMMAND biz_ai_role_test)

add_test(NAME biz_log_compile_matrix
COMMAND "${Python3_EXECUTABLE}" "${BIZ_CLIENT_DIR}/test/test_log_compile.py"
--cc "${CMAKE_C_COMPILER}")

set(AI_TCP_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/rtc-tcp-client/test")
set(IOT_CLIENT_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/iot-client/test")
set(TUYA_BLE_TESTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/tuya-ble/test")
Expand Down Expand Up @@ -573,6 +614,52 @@ if(AGENTIC_KIT_BUILD_TESTS AND AGENTIC_KIT_ENABLE_PROJECT_TESTS)
)
target_link_libraries(tuya_ble_test PRIVATE tuya_iot_client_testlog)
add_test(NAME tuya_ble_test COMMAND tuya_ble_test)

if(AGENTIC_KIT_ENABLE_LAN)
add_executable(tuya_lan_service_test "${TUYA_LAN_DIR}/test/test_service.c")
target_include_directories(tuya_lan_service_test PRIVATE "${TUYA_LAN_DIR}/src")
target_link_libraries(tuya_lan_service_test PRIVATE tuya_lan)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(tuya_lan_service_test PRIVATE -Wall -Wextra -Werror)
endif()
add_test(NAME tuya_lan_service_test COMMAND tuya_lan_service_test)
# The independent peer harness supplies a deterministic/failing RNG; do not
# link iot-client/rng.c into this target or rebind a process-wide allocator.
add_executable(tuya_lan_protocol_test
"${TUYA_LAN_DIR}/src/tuya_lan_protocol.c"
"${TUYA_LAN_DIR}/test/test_protocol.c")
target_include_directories(tuya_lan_protocol_test PRIVATE
"${TUYA_LAN_DIR}/include" "${TUYA_LAN_DIR}/src" "${cjson_source_dir}")
target_link_libraries(tuya_lan_protocol_test PRIVATE agentic_kit_common
${AGENTIC_KIT_CJSON_TARGET})
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(tuya_lan_protocol_test PRIVATE -Wall -Wextra -Werror)
endif()
add_test(NAME tuya_lan_protocol_test COMMAND tuya_lan_protocol_test)
add_executable(tuya_lan_peer_device "${CMAKE_CURRENT_SOURCE_DIR}/examples/posix/lan/lan_demo.c")
target_link_libraries(tuya_lan_peer_device PRIVATE tuya_lan tuya_iot_client)
add_test(NAME tuya_lan_peer_test COMMAND "${Python3_EXECUTABLE}"
"${TUYA_LAN_DIR}/test/test_peer.py" "$<TARGET_FILE:tuya_lan_peer_device>")
add_executable(pal_net_test "${PAL_DIR}/test/test_net.c")
target_link_libraries(pal_net_test PRIVATE agentic_kit_pal)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(pal_net_test PRIVATE -Wall -Wextra -Werror)
endif()
add_test(NAME pal_net_test COMMAND pal_net_test)
# Exercise the same unified network contract in the actual FreeRTOS
# port against host kernel/lwIP declarations, not a second net adapter.
add_executable(pal_freertos_net_test
"${PAL_DIR}/pal_freertos.c" "${PAL_DIR}/test/test_net.c")
target_include_directories(pal_freertos_net_test PRIVATE
"${PAL_DIR}/test" "${PAL_DIR}" "${COMMON_DIR}")
target_compile_definitions(pal_freertos_net_test PRIVATE
PAL_TEST_FREERTOS=1 TEST_TICK_BITS=32 TEST_TICK_HZ=1000
AGENTIC_KIT_ENABLE_LAN=1 AGENTIC_KIT_LOG_LEVEL=0)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(pal_freertos_net_test PRIVATE -Wall -Wextra -Werror)
endif()
add_test(NAME pal_freertos_net_test COMMAND pal_freertos_net_test)
endif()
endif()

# -----------------------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions CONTEXT-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and decisions.
block in `modules/rtc-tcp-client/include/tuya_ai.h`.
- [Tuya BLE](./modules/tuya-ble/CONTEXT.md) — device ↔ app BLE provisioning: advertising,
the pairing handshake, and WiFi-credential delivery.
- [Tuya LAN](./modules/tuya-lan/CONTEXT.md) — activated device ↔ app local control:
discovery, authenticated LAN 3.5 Sessions, DP requests and status reports.

## Relationships

Expand All @@ -33,3 +35,7 @@ and decisions.
against the cloud.
- All contexts share the Platform Abstraction Layer (`pal/pal.h`) for sockets, memory,
mutex, threads, and time.
- **Tuya LAN → IoT Client**: the application supplies Activation credentials and
connects LAN callbacks to the IoT Client's public local-DP input and DP state.
LAN reports do not publish MQTT or clear MQTT dirty bits; the application owns
channel reporting. Optional server/datagram operations share `pal_t` in `pal/pal.h`.
17 changes: 17 additions & 0 deletions common/bounded_string.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef AGENTIC_KIT_BOUNDED_STRING_H
#define AGENTIC_KIT_BOUNDED_STRING_H

#include <stdbool.h>
#include <stddef.h>

/* Require a nonempty string terminated within capacity bytes, including its
* NUL. Read at most capacity bytes, stopping at the first NUL. NULL, zero
* capacity, empty and unterminated strings are invalid. */
static inline bool bounded_string_nonempty(const char *text, size_t capacity)
{
if (!text || !capacity || !text[0]) return false;
for (size_t i = 1; i < capacity; ++i) if (!text[i]) return true;
return false;
}

#endif
Loading
Loading