RTC TCP Client SDK Reference

1. Overview​

RTC TCP Client (API prefix tai_*) is the TCP implementation of tRTC (Tuya's proprietary RTC protocol). It is provided as source code and supports cross-platform porting through the PAL (Platform Abstraction Layer), making it suitable for environments such as POSIX and FreeRTOS (ESP-IDF).

Header file: tuya_ai.h (single include)

Features​

  • Simple API: Typed send functions (tai_send_text, tai_send_audio_*, and tai_send_image) eliminate the need to manually assemble data structures
  • Background receive thread: Automatically starts a background thread after tai_connect to handle receiving and Keepalive
  • Callback-driven: Receives data through on_audio, on_text, on_image, on_event, and on_disconnect
  • No external dependencies: The user-provided PAL only needs to supply raw TCP and platform primitives; the SDK handles TLS and cryptography internally through bundled mbedTLS

2. Constant Definitions​

2.1 Packet Types​

MacroValueDescription
TAI_PKT_CLIENT_HELLO1Client handshake
TAI_PKT_AUTHENTICATE_RESPONSE3Server authentication result for ClientHello
TAI_PKT_PING4Keepalive Ping request
TAI_PKT_PONG5Keepalive Pong response
TAI_PKT_CONNECTION_CLOSE6Close Connection
TAI_PKT_SESSION_NEW7Create Session
TAI_PKT_SESSION_CLOSE8Close Session
TAI_PKT_CONNECTION_REFRESH_REQ9Connection refresh request
TAI_PKT_CONNECTION_REFRESH_RESP10Connection refresh response
TAI_PKT_VIDEO30Video data
TAI_PKT_AUDIO31Audio data
TAI_PKT_IMAGE32Image data
TAI_PKT_FILE33File data
TAI_PKT_TEXT34Text data
TAI_PKT_EVENT35Event data

2.2 Stream Flags​

MacroValueDescription
TAI_STREAM_ONE_SHOT0x00Single Packet (complete data)
TAI_STREAM_START0x01Stream start
TAI_STREAM_MIDDLE0x02Stream middle
TAI_STREAM_END0x03Stream end

2.3 Event Types​

MacroValueDescription
TAI_EVT_START0Session start
TAI_EVT_PAYLOADS_END1End of payloads
TAI_EVT_END2Session end
TAI_EVT_ONE_SHOT3One-shot Event
TAI_EVT_CHAT_BREAK4Chat break/cloud VAD turn end (the user stops speaking or interrupts mid-response); the current cloud uses this as the turn-boundary signal; only clear the downlink TTS buffer/playback queue and do not end the uplink Event
TAI_EVT_SERVER_VAD5Server VAD detects that the user has stopped speaking (legacy server signal; the current cloud no longer sends it, and the constant is retained only for protocol compatibility)
TAI_EVT_MCP_CMD1000MCP command (executed on the device)
TAI_EVT_SERVER_TIMEOVER1001Server timeout
TAI_EVT_UPDATE_CONTEXT1002Context update

2.4 Client Types​

MacroValueDescription
TAI_CLIENT_DEVICE1Device
TAI_CLIENT_APP2App

2.5 Audio Codecs​

MacroValueDescription
TAI_AUDIO_PCM101Raw PCM
TAI_AUDIO_OPUS111Opus encoding

2.6 Image Formats​

MacroValueDescription
TAI_IMG_JPEG1JPEG
TAI_IMG_PNG2PNG

2.7 Image Payload Types​

MacroValueDescription
TAI_IMG_PAYLOAD_RAW0Raw binary
TAI_IMG_PAYLOAD_BASE641Base64 encoding
TAI_IMG_PAYLOAD_URL2URL string

2.8 Sign Levels​

MacroValueDescription
TAI_SIGN_NONE0No signature
TAI_SIGN_HMAC_SHA11HMAC-SHA1
TAI_SIGN_HMAC_SHA2562HMAC-SHA256 (recommended)

2.9 Data IDs​

MacroValueDescription
TAI_DATA_ID_AUDIO_UP1Uplink audio
TAI_DATA_ID_AUDIO_DOWN2Downlink audio
TAI_DATA_ID_TEXT_UP3Uplink text
TAI_DATA_ID_TEXT_DOWN4Downlink text
TAI_DATA_ID_IMAGE_UP5Uplink image
TAI_DATA_ID_AUDIO_AUX7Auxiliary audio

2.10 Return Codes​

MacroValueDescription
TAI_OK0Success
TAI_ERR_ARGS-1Invalid argument
TAI_ERR_MEM-2Insufficient memory
TAI_ERR_NET-3Network error
TAI_ERR_TLS-4TLS error
TAI_ERR_PROTO-5Protocol error
TAI_ERR_HMAC-6HMAC verification failed
TAI_ERR_AGAIN-7Retry required
TAI_ERR_CRYPTO-8Encryption/decryption error

3. Configuration (tai_config_t)​

Populate this structure before calling tai_ctx_init. All pointer fields must remain valid for the lifetime of the context.

3.1 Server Configuration​

FieldTypeDescription
hostconst char *Server address (usually parsed from the token returned by iot_client_get_session_token())
portuint16_tServer port
tls_sniconst char *TLS SNI hostname (usually the same as host; if the token provides a domain name, prefer the domain name)

3.2 Identity Configuration​

FieldTypeDescription
device_idconst char *Device ID (devid obtained after Provisioning)
local_keyconst char *Local key (local_key obtained after Provisioning)
client_typeuint8_tClient type: TAI_CLIENT_DEVICE or TAI_CLIENT_APP (0 = default TAI_CLIENT_DEVICE)
protocol_versionuint8_tProtocol version: use TAI_VER_21 (0 = default TAI_VER_21)

3.3 Session Options​

FieldTypeDescription
session_attrs_jsonconst char *Session attributes JSON (NULL uses the default). Can configure Server VAD, audio format, and other options
event_user_data_jsonconst char *Event user data JSON (NULL uses the default)
agent_tokenconst char *Agent Token (specifies a particular Agent; NULL uses the product's default Agent)

3.4 Business Identifiers​

FieldTypeDescription
biz_codeuint32_tBusiness code (0 = default 65537)
biz_taguint64_tBusiness tag (0 = default 119)

3.5 Security Configuration​

FieldTypeDescription
sign_leveluint8_tSign level: TAI_SIGN_HMAC_SHA256 (recommended) or TAI_SIGN_HMAC_SHA1. Note that TAI_SIGN_NONE (0) is treated as "unset" and forcibly falls back to TAI_SIGN_HMAC_SHA256; signing cannot be disabled through this field

3.6 Keepalive Configuration​

FieldTypeDescription
ping_interval_msuint32_tPing interval (0 = default 60000ms)
ping_timeout_msuint32_tReceive-liveness timeout refreshed by any inbound data (0 = default 90000ms); intentional backpressure suspends it and resume grants a fresh full budget
connect_timeout_msuint32_tConnection timeout (0 = default 5000ms). It separately constrains the two sequential wait stages of tai_connect: first Connection establishment (TCP connection + TLS handshake, sharing one budget), then the server's SessionNew response. A timeout in either stage means the Connection failed, so the worst-case duration of tai_connect is approximately twice this value.

3.7 Test Configuration​

FieldTypeDescription
disable_tlsuint8_tIf nonzero, skips TLS; for integration testing only

3.8 Platform Adaptation​

FieldTypeDescription
palconst pal_t *Pointer to the platform adaptation layer implementation
cert_bundle_attachtls_cert_bundle_attach_fnPlatform certificate bundle callback (set to esp_crt_bundle_attach on ESP-IDF; otherwise TLS does not verify certificates); NULL means unused. See TLS Certificate Verification

3.9 Callbacks​

All callbacks are invoked on the background receive thread.

FieldTypeDescription
on_audiofunction pointerAudio data callback
on_textfunction pointerText data callback
on_imagefunction pointerImage data callback (images generated by the cloud)
on_eventfunction pointerEvent callback (MCP commands, Chat break, VAD, and so on)
on_disconnectfunction pointerDisconnect callback
on_flow_controlint (*)(tai_ctx_t *, void *)Optional receive-backpressure hook; nonzero admits receive, 0 pauses reads and parsing, NULL disables backpressure
user_datavoid *Passed through to all callbacks

Callback signatures:

Each callback receives a single pointer to a const message structure plus user_data (instead of the former multi-argument form).

void (*on_audio) (tai_ctx_t *ctx, const tai_audio_msg_t *msg, void *user_data);
void (*on_text) (tai_ctx_t *ctx, const tai_text_msg_t *msg, void *user_data);
void (*on_image) (tai_ctx_t *ctx, const tai_image_msg_t *msg, void *user_data);
void (*on_event) (tai_ctx_t *ctx, const tai_event_msg_t *msg, void *user_data);
void (*on_disconnect)(tai_ctx_t *ctx, const tai_disconnect_msg_t *msg, void *user_data);
int (*on_flow_control)(tai_ctx_t *ctx, void *user_data);

Receive Backpressure (on_flow_control)​

  • The worker calls the hook before each read and between complete Frames. A conforming server never consults it during the connect handshake (the ack precedes media); only a server coalescing media ahead of the ack could. The hook must not block; the application synchronizes shared queue state.
  • Returning 0 pauses both reads and parsing, stalling all inbound traffic including ChatBreak, ASR text, Pong, and EOF detection. Resume processes complete buffered Frames before reading again; partial input returns to bounded blocking receive.
  • An intentional pause suspends receive-liveness timeout accounting, and resume grants a fresh ping_timeout_ms budget. Ping and stop requests remain active; Ping send failure still disconnects.
  • Admission is also checked before each codec-frame callback within an Audio Packet. A mid-Packet pause retains that Packet's remaining bytes without copying; resume delivers the remaining frames before any later Packet. Applications discard obsolete audio inside on_audio by comparing the server timestamp (msg->timestamp_ms), not by mutating SDK receive state.

Backpressure is rechecked every AGENTIC_KIT_TAI_FLOW_CONTROL_POLL_MS (50 ms by default), and sustained receive traffic yields CPU for AGENTIC_KIT_TAI_WORKER_YIELD_MS (10 ms by default).

pal_t adds a mandatory sleep_ms callback. It must sleep without busy-waiting for at least the requested milliseconds, treat 0 as a no-op, and not depend on socket readability. Recompile all custom PALs and consumers against the updated header.

Received Message Structures​

tai_audio_msg_t (audio callback):

FieldTypeDescription
dataconst uint8_t *Opus frame / PCM bytes
lensize_tData length in bytes
codecuint8_tTAI_AUDIO_OPUS / TAI_AUDIO_PCM / 0=unknown
sample_rateuint32_tSample rate (Hz), 0=unknown
frame_durationuint16_tDuration of each Opus frame (ms)
stream_flaguint8_tTAI_STREAM_* (from the media header)
data_iduint16_tData ID: AUDIO_DOWN(2) / AUDIO_AUX(7)
event_idconst char *Turn ID (borrowed); "" if absent
timestamp_msuint64_tServer media-header timestamp, not local time; latch START's value to filter a stream against an interruption time

tai_text_msg_t (text callback):

FieldTypeDescription
textconst char *UTF-8 text, not NUL-terminated
lensize_tText length in bytes
stream_flaguint8_tTAI_STREAM_*
data_iduint16_tData ID: TAI_DATA_ID_TEXT_DOWN(4)
sequint32_tText sequence number within each Event (varint)
event_idconst char *Turn ID (borrowed); "" if absent

tai_image_msg_t (image callback):

FieldTypeDescription
dataconst uint8_t *Encoded image bytes (JPEG/PNG); valid for the callback lifetime
lensize_tData length in bytes
formatuint8_tTAI_IMG_JPEG / TAI_IMG_PNG / 0=unknown
widthuint16_tWidth (px), 0 if unknown or if this is not a START/ONE_SHOT Packet
heightuint16_tHeight (px), 0 if unknown or if this is not a START/ONE_SHOT Packet
stream_flaguint8_tTAI_STREAM_*
data_iduint16_tData ID (from the media header)
event_idconst char *Turn ID (borrowed); "" if absent
timestamp_msuint64_tStream start timestamp (media header)
Streaming reception

Received images arrive as a stream of chunks: START (or ONE_SHOT) carries the first bytes and image-params, MIDDLE continues the stream, and END finishes it (len may be 0). The caller accumulates the chunks according to the stream_flag and decodes the complete image after the stream ends (END or ONE_SHOT). format/width/height are parsed from image-params only for START/ONE_SHOT and are 0 for MIDDLE/END.

tai_event_msg_t (Event callback):

FieldTypeDescription
event_typeuint16_tTAI_EVT_*
dataconst uint8_t *Event payload (usually JSON)
lensize_tPayload length in bytes
event_idconst char *attr 61 (borrowed); "" if absent
user_dataconst uint8_t *attr 111 (borrowed), not NUL-terminated; NULL if absent
user_data_lensize_tuser_data length in bytes; separate from the event payload, and the SDK does not parse its JSON
ChatBreak interruption time

The server time for TAI_EVT_CHAT_BREAK is not in the event payload but in attr 111: {"breakAttributes":{"time":"<server-time>"}}, a server epoch in milliseconds — the same clock and unit as on_audio's timestamp_ms. Read msg->user_data, parse breakAttributes.time, and compare it with the timestamp_ms latched in on_audio to decide whether a stream is obsolete. The MQTT asrInterrupt arrives by a different path: its time is the time field of its own payload, and it is the same server time value.

tai_disconnect_msg_t (disconnect callback):

FieldTypeDescription
reasonuint8_tTAI_DISCONNECT_* (see the table that follows)
close_codeuint16_tServer close code (SESSION/CONNECTION); 0 in other cases
detailuint8_tTAI_TRANSPORT_* / TAI_PROTO_ERR_* (see the tables that follow); 0 in other cases
connection_aliveuint8_t1 only when reason==SESSION_CLOSE
session_idchar[64]Copied value; "" if absent
Lifetime

msg and all pointers within it (data / text / event_id, and so on) are valid only for the duration of the callback - the SDK never heap-allocates them. Copy them if they must be retained after the callback returns.

Disconnect Reasons (reason)​

MacroValueDescription
TAI_DISCONNECT_SESSION_CLOSE0Server SessionClose; the Connection may remain alive (connection_alive==1)
TAI_DISCONNECT_CONNECTION_CLOSE1Server ConnectionClose; the worker stops
TAI_DISCONNECT_TRANSPORT2The worker detects a transport-layer failure
TAI_DISCONNECT_PROTOCOL3Fail-fast: parsing/behavior error

detail (when reason==TRANSPORT)​

MacroValueDescription
TAI_TRANSPORT_PING_TIMEOUT1Ping timeout
TAI_TRANSPORT_EOF2Peer closed the Connection (EOF)
TAI_TRANSPORT_NET_ERROR3Network error

detail (when reason==PROTOCOL)​

MacroValueDescription
TAI_PROTO_ERR_BAD_VERSION1Unknown leading Frame byte (desynchronization)
TAI_PROTO_ERR_HMAC2Frame HMAC verification failed
TAI_PROTO_ERR_FRAME_DECODE3Frame header decode failed
TAI_PROTO_ERR_FRAG4Orphan MIDDLE/LAST, overflow, or oversized data
TAI_PROTO_ERR_PKT_DECODE5Malformed application Packet / attribute block
TAI_PROTO_ERR_UNKNOWN_PKT6Unknown Packet type (strict mode)
TAI_PROTO_ERR_EVENT7Event unpacking failed / unknown event type
TAI_PROTO_ERR_MEDIA_HDR8Truncated media/text header
TAI_PROTO_ERR_UNEXPECTED9Valid Packet in an invalid state (behavior error)
TAI_PROTO_ERR_OVERSIZED10Inbound Frame exceeds rx_buf

4. Lifecycle API​

tai_ctx_size​

size_t tai_ctx_size(void);

Returns the amount of memory required by the context. The caller must allocate at least this many bytes and pass the memory to tai_ctx_init.


tai_ctx_init​

tai_ctx_t *tai_ctx_init(void *mem, const tai_config_t *cfg);

Initializes the context. mem must be at least tai_ctx_size() bytes and remain valid for the lifetime of the context.

Parameters:

  • mem - Preallocated memory block
  • cfg - Configuration structure

Return value: Returns tai_ctx_t* (that is, mem cast to this type) on success, or NULL on failure.


tai_ctx_deinit​

void tai_ctx_deinit(tai_ctx_t *ctx);

Deinitializes the context and releases internal resources. mem can be freed after this call. It must be called after tai_disconnect.


tai_connect​

int tai_connect(tai_ctx_t *ctx);

Performs the TLS handshake, sends ClientHello, establishes a Session, and starts the background receive thread.

Return value: TAI_OK on success, or TAI_ERR_* on failure.

Behavior:

  • Blocks until the Connection is established or fails
  • After success, the background thread begins processing Ping/Pong and receiving data
  • The tai_send_* functions can be called once the Connection succeeds

tai_disconnect​

void tai_disconnect(tai_ctx_t *ctx);

Stops and joins the background receive thread first, then sends a best-effort SessionClose, and finally closes the transport (TLS / TCP).

Behavior:

  • Blocks until the background thread exits (join)
  • Data cannot be sent after this call
  • Call tai_ctx_deinit afterward
  • Must not be called from a callback (it would join the worker thread that is executing the callback, causing a self-deadlock); to trigger disconnection from a callback, use tai_request_disconnect

tai_request_disconnect​

void tai_request_disconnect(tai_ctx_t *ctx);

Requests the background worker to stop, but does not join it or tear down the Connection. It can be called from any thread, including from within a receive callback (unlike tai_disconnect). The worker exits on its next loop iteration.

Purpose: Safely triggers disconnection from a callback. After this call, the thread that owns the context must still call tai_disconnect() to join the worker, send SessionClose, and release resources.


5. Send API​

tai_send_text​

int tai_send_text(tai_ctx_t *ctx, const char *text, size_t len);

Sends a text message. Internally, one call sends four application Packets in sequence: EventStart -> text (with the ONE_SHOT stream flag) -> EventPayloadsEnd -> EventEnd; the caller does not need to finish the sequence manually.

Parameters:

  • text - UTF-8 text
  • len - Text length in bytes

Return value: TAI_OK or TAI_ERR_*.


tai_send_audio_start​

int tai_send_audio_start(tai_ctx_t *ctx,
uint8_t codec, uint8_t channels,
uint8_t bit_depth, uint32_t sample_rate);

Starts an audio stream. It must be called before tai_send_audio_chunk.

Server VAD mode

When Server VAD (continuous conversation) is enabled, call this function only once for the entire chat Session, not once per turn; keep the uplink audio stream open until the Session ends. See the "Server VAD vs. device-side VAD" comparison table in VAD and Interruption.

Parameters:

  • codec - Encoding format: TAI_AUDIO_PCM (101) or TAI_AUDIO_OPUS (111)
  • channels - Number of channels (usually 1)
  • bit_depth - Bit depth (usually 16)
  • sample_rate - Sample rate (usually 16000)

tai_send_audio_chunk​

int tai_send_audio_chunk(tai_ctx_t *ctx, const uint8_t *pcm, size_t len);

Sends one chunk of audio data. It can be called repeatedly (for middle Packets in the stream).

Parameters:

  • pcm - Audio data (a PCM or Opus frame, depending on the codec passed to audio_start)
  • len - Data length in bytes

tai_send_audio_end​

int tai_send_audio_end(tai_ctx_t *ctx);

Ends the audio stream. Notifies the server that the current audio input is complete and processing can begin.

Do not call in Server VAD mode

When Server VAD (asr.enableVad) is enabled, this function must not be called. The current cloud's turn-end signal is TAI_EVT_CHAT_BREAK (TAI_EVT_SERVER_VAD is no longer sent); do not call this function when that signal is received either. This function is only for manual push-to-talk or device-side local VAD stop detection. Calling it incorrectly actively ends the current uplink Event, causing the cloud to truncate the user's speech. See VAD and Interruption.


tai_send_image​

int tai_send_image(tai_ctx_t *ctx,
const uint8_t *data, size_t len,
uint8_t format, uint16_t width, uint16_t height);

Sends a single image.

Parameters:

  • data - Image binary data
  • len - Data length
  • format - Image format: TAI_IMG_JPEG (1) or TAI_IMG_PNG (2)
  • width / height - Image dimensions

tai_send_image_with_text​

int tai_send_image_with_text(tai_ctx_t *ctx,
const char *text, size_t text_len,
const uint8_t *img_data, size_t img_len,
uint8_t format,
uint16_t width, uint16_t height);

Sends text and an image together (for example, in an image-understanding scenario). Internally, the text Packet is sent before the image Packet to form one complete request.


tai_chat_break​

int tai_chat_break(tai_ctx_t *ctx);

Sends a Chat break Event. This is used when the user actively interrupts the AI response (for example, with a button). After receiving it, the server stops generating the current response.


tai_send_mcp_response​

int tai_send_mcp_response(tai_ctx_t *ctx, const char *json_rpc_response);

Sends the response to an MCP command. When on_event receives TAI_EVT_MCP_CMD, the device executes the command and returns the result through this function.

Parameters:

  • json_rpc_response - Response string in JSON-RPC format

6. Logging​

The SDK's global log switch is the compile-time AGENTIC_KIT_LOG_LEVEL (gated once in common/log.h; it applies to the entire SDK, not only this module); this module can additionally be lowered alone with AGENTIC_KIT_TAI_LOG_LEVEL (the renamed former TAI_LOG_LEVEL — lower-only, defaulting to the SDK-wide value). Valid values:

ValueMeaning
0Off entirely
1TAI_LOG_ERROR
2TAI_LOG_WARN
3TAI_LOG_INFO
4TAI_LOG_DEBUG (default)

Log lines above the ceiling are eliminated at compile time; below it they emit unconditionally — there is no runtime level (tai_set_log_level() was removed together with the runtime layer). Compile production builds with -DAGENTIC_KIT_LOG_LEVEL=2 to keep only error + warn, or lower only this module with -DAGENTIC_KIT_TAI_LOG_LEVEL=N (below 3, the tai_log_packet JSON formatter and its calls vanish together); changing where lines go (or dropping them by level) is a build decision as well: define AGENTIC_KIT_LOG and take over the dispatch with your own macro.

Media middle frames log at INFO with 1/N sampling by default (AGENTIC_KIT_TAI_LOG_MEDIA_SAMPLE_N, default 50; set it to 0 to disable sampling and enter flood mode). In flood mode middle frames emit at DEBUG and only when the effective ceiling is 4 — with a ceiling of 3, setting sampling to 0 leaves middle frames with no logs at all (fewer than the default sampling; do not mistake it for packet loss).


7. Typical Usage Flow​

#include "tuya_ai.h"

// 1. Allocate memory
void *mem = malloc(tai_ctx_size());

// 2. Configure
tai_config_t cfg = {
.host = "ai-gw.example.com",
.port = 8883,
.device_id = devid,
.local_key = local_key,
.client_type = TAI_CLIENT_DEVICE,
.protocol_version = TAI_VER_21,
.sign_level = TAI_SIGN_HMAC_SHA256,
.pal = &my_pal,
.on_audio = my_on_audio,
.on_text = my_on_text,
.on_image = my_on_image,
.on_event = my_on_event,
.on_disconnect = my_on_disconnect,
};

// 3. Initialize + connect
tai_ctx_t *ctx = tai_ctx_init(mem, &cfg);
if (tai_connect(ctx) != TAI_OK) { /* handle error */ }

// 4. Send text
tai_send_text(ctx, "hello", 5);

// 5. Or send an audio stream
tai_send_audio_start(ctx, TAI_AUDIO_PCM, 1, 16, 16000);
while (has_audio) {
tai_send_audio_chunk(ctx, pcm_frame, frame_len);
}
tai_send_audio_end(ctx); // Finish in manual mode; do not call audio_end in Server VAD mode

// 6. Receive responses asynchronously through callbacks...

// 7. Clean up
tai_disconnect(ctx);
tai_ctx_deinit(ctx);
free(mem);

The audio-stream call pattern depends on the VAD mode (continuous conversation with Server VAD vs. device-side VAD/manual push-to-talk). See the comparison table in VAD and Interruption.


8. Thread Safety​

  • The tai_send_* functions are thread-safe and can be called from any thread
  • All callbacks are invoked sequentially on the same background receive thread
  • tai_send_*() can be called from a callback (no lock is held)
  • tai_connect, tai_disconnect, and tai_ctx_deinit must not be called from a callback (they join the worker thread, causing a self-deadlock)
  • To trigger disconnection from a callback, call tai_request_disconnect(), then have the thread that owns the context call tai_disconnect()
© 2026 Tuya Inc.Tuya Physical AI · Agentic-kit