BLE Provisioning

Corresponding example: examples/esp-idf/pair/pair-by-ble/

This chapter explains how to Provision a device over BLE (Bluetooth Low Energy) on an embedded device. The Tuya App passes the WiFi credentials and Provisioning token to the device over a BLE connection, so the device can complete Provisioning without a camera or a screen.

Applicable scenarios​

  • The device supports Bluetooth (BLE 4.2+, NimBLE stack)
  • The device has no camera or screen but needs to be Provisioned through the Tuya App
  • The device is not yet connected to WiFi (the core purpose of BLE Provisioning is to deliver WiFi credentials)
Platform note

The current example is based on ESP-IDF + the NimBLE stack; other platforms must adapt the BLE layer themselves.

Overall flow​

nvs_flash_init() // 1. Initialize NVS (required by BLE)
|
v
tuya_ble_nimble_start(&prov_cfg) // 2. Start BLE advertising and wait for the App to connect
|
v
(The App sends the WiFi SSID/password/Token over BLE)
|
v
on_tuya_ble_prov_complete(creds) // 3. Callback: WiFi credentials and Token received
|
v
tuya_ble_nimble_stop() // 4. Stop BLE advertising
|
v
(Connect to the network using the WiFi credentials) // 5. Connect to WiFi
|
v
iot_client_init_on_boarding_with_token(token) // 6. Activate the device with the Token
The device must connect to MQTT before the App considers Provisioning successful

The App considers Provisioning successful only after it detects that the device has connected to the Tuya cloud MQTT channel (the device is online). Completing only Token Activation and obtaining credentials such as devid without connecting to MQTT makes the App show Provisioning failure/timeout.

Therefore this is now guaranteed by the default behavior: auto-connect is enabled by default and needs no extra configuration. As long as .mqtt_disable_auto_connect is not set, the device automatically connects to MQTT after Activation completes (the example main/main.c is already configured this way); only if the application explicitly sets .mqtt_disable_auto_connect = true must it call iot_client_connect() manually immediately after Activation succeeds.

Key code​

Configuration and startup​

#include "tuya_ble_nimble.h"
#include "app_config.h"

static void on_tuya_ble_prov_complete(const tuya_ble_wifi_creds_t *creds)
{
// WiFi credentials received from the App
// creds is valid only during the callback; copy it into an application-owned buffer
// (the example logs at DEBUG, so the SDK's [ble] protocol log prints the raw
// credentials JSON including the password/Token, which helps on-device debugging)
s_wifi_creds = *creds;
// Notify the main thread to continue
xEventGroupSetBits(s_prov_event_group, PROV_DONE_BIT);
}

void app_main(void)
{
// Initialize NVS (required by NimBLE)
nvs_flash_init();
iot_init_default(); // Initialize the PAL/cJSON allocator before BLE JSON parsing

tuya_ble_prov_cfg_t prov_cfg = {
.device_name = "TYBLE", // BLE advertising name (max 5 characters; longer names are truncated)
.product_key = PRODUCT_KEY, // Product PID
.uuid = DEVICE_UUID, // Device UUID
.auth_key = AUTH_KEY, // Device Auth Key
.cb = on_tuya_ble_prov_complete,
};

tuya_ble_nimble_start(&prov_cfg);

// Wait for Provisioning to complete
xEventGroupWaitBits(s_prov_event_group, PROV_DONE_BIT,
pdTRUE, pdFALSE, portMAX_DELAY);

tuya_ble_nimble_stop();

// Next: connect to WiFi using creds->ssid/password
// Then call iot_client_init_on_boarding_with_token() with creds->token
}

Configuration file app_config.h​

#define TUYA_BLE_DEVICE_NAME "TYBLE" // max 5 characters (TUYA_BLE_NAME_MAX_LEN); longer values are truncated
#define PRODUCT_KEY "your_product_key"
#define DEVICE_UUID "your_uuid"
#define AUTH_KEY "your_auth_key"

Note: the current example reads these configuration items directly from main/app_config.h; refer to the actual example file in the repository.

API reference​

tuya_ble_nimble_start​

int tuya_ble_nimble_start(const tuya_ble_prov_cfg_t *cfg);

Starts BLE advertising and the GATT service, and waits for the Tuya App to connect and deliver Provisioning information.

Return value: 0 on success; nonzero indicates an error.

tuya_ble_prov_cfg_t fields:

FieldTypeDescription
device_nameconst char *BLE advertising device name (max 5 characters, TUYA_BLE_NAME_MAX_LEN; the excess is silently truncated)
product_keyconst char *Product PID
uuidconst char *Device UUID
auth_keyconst char *Device Auth Key
cbcallbackProvisioning-complete callback

tuya_ble_nimble_stop​

int tuya_ble_nimble_stop(void);

Stops BLE advertising and services, waits for any already-accepted WiFi scan task to finish, then releases the queues and NimBLE resources. Start/stop must be called serially by the application task; stop must not be called from a BLE callback or the ESP event loop. Scans are cancelled logically, so if the driver does not return for a long time, stop also waits.

Return value: 0 means stopped (calling stop again also returns 0); nonzero means nimble_port_stop() failed and resources are kept so it can be retried; a failure of nimble_port_deinit() (which is almost impossible after a clean stop) aborts the program through ESP_ERROR_CHECK. After a successful stop, the example first stops the WiFi started during the scan phase, then configures the credentials and restarts STA, without relying on calling start again on an already-started STA to produce a connection event.

tuya_ble_wifi_creds_t​

The struct received in the callback:

FieldTypeDescription
ssidchar[]WiFi SSID
passwordchar[]WiFi password
tokenchar[]Provisioning token (used for subsequent device Activation)

Build and run​

cd examples/esp-idf/pair/pair-by-ble
idf.py set-target esp32s3 # Choose according to your chip
idf.py build
idf.py flash monitor

sdkconfig highlights​

Key settings currently present in sdkconfig.defaults:

  • CONFIG_BT_ENABLED=y — Enable Bluetooth
  • CONFIG_BT_NIMBLE_ENABLED=y — Use the NimBLE stack
  • CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y — Enable BLE controller mode only

Notes​

  • After BLE Provisioning completes, stop BLE advertising (tuya_ble_nimble_stop) as soon as possible to avoid RF conflicts when coexisting with WiFi.
  • The token format is the same as for the other Provisioning methods: the first two characters are the Region code.
  • The device Activation flow after Provisioning completes is the same as in Device QR-code Provisioning, using iot_client_init_on_boarding_with_token().
  • Never set .mqtt_disable_auto_connect = true in the Activation configuration: the App treats the device coming online on MQTT as the condition for Provisioning success, so without an MQTT connection the App shows Provisioning failure/timeout.
  • Make sure the project correctly references the modules/tuya-ble/ and modules/iot-client/ components.

BLE core porting conventions​

Configuration strings must remain valid for the lifetime of the core state and be NUL-terminated: product_key is 16 bytes, auth_key is 32 bytes, and uuid is 16 bytes or 20 alphanumeric characters. After upgrading, recompile callers, because the layout of the public state struct has changed.

Call tuya_ble_prov_on_data(), tuya_ble_prov_tx_ready(), tuya_ble_prov_tick(), and tuya_ble_prov_close() on the same BLE execution context. On connection close, reconnect, or stack reset, call close() to clear the session keys and queued data; the legacy reset_conn() only resets transport state. set_paired(true) does not grant permission to deliver credentials.

Notifications are at most 20 bytes by default; after MTU negotiation, update the actual budget with tuya_ble_prov_set_gatt_payload(state, mtu - 3). The send callback must copy the data before returning: returning 0 means accepted, TUYA_BLE_SEND_BUSY means not accepted and retry later, and any other value means permanent failure. The port periodically passes in a monotonic millisecond time; incomplete transmissions expire after 10 seconds. The NimBLE example drives retries and timeouts on its event queue without adding an MQTT thread. The credential callback runs only after the reply has been accepted by the transport layer; while waiting, new credential requests are ignored, the pending credentials are not replaced, and no ACK is sent for the new request. Trsmitr out-of-order and timeout diagnostics are emitted through the SDK log facade.

tuya_ble_prov_cfg_ext_t.random_fn is optional and provides a CSPRNG that reports the number of bytes filled; short output fails the handshake. When it is not provided, tuya_ble_hal_random() must fill the buffer completely.

Configuring a scan provider enables the WiFi-list and status-query capability declarations; the NimBLE example has a provider configured. The port's worker task performs the WiFi scan, copying the original scan token and results through queues, and the BLE execution context delivers them to the SDK. Cancellation does not immediately interrupt the radio scan; until the old task has finished and been handled, no new scan is accepted, so stale results cannot misappropriate a new token. The status query returns only a fixed CFG status; there is no active stage reporting or a complete PSK3.0 activation exchange. Credentials are still received through the legacy token Provisioning flow; completing BLE credential reception does not mean cloud Activation has completed.

© 2026 Tuya Inc.Tuya Physical AI · Agentic-kit