This guide connects an ESP32 to Thinger.io from an ESP-IDF project, without the Arduino framework. You add the IOTMP client component, set the credentials, expose device resources and store data in a Data Bucket.
Before you start
- A Thinger.io account. Your server is
YOUR_SERVER:iot.thinger.ioon the free Cloud account, or your own domain on a private instance. - ESP-IDF 5.x or 6.x, with
idf.pyin your shell. - An ESP32 board, such as an ESP32, ESP32-S3 or ESP32-C3.
Steps
Create the device
Console
Go to Devices and click Add Device. Choose IOTMP Device (Thinger.io protocol) as Device Type, set the Device Id to
YOUR_DEVICE, click Random in Device Credentials and copy the credentials. Click Add Device.CLI
bashthinger device create YOUR_DEVICE -t GenericAI
Ask your AI agentCreate an IOTMP device called YOUR_DEVICE in my Thinger.io account and show me its credentials.
Add the components
The client is two components:
iotmp-espidfand the protocol core it uses,iotmp-embedded. Clone both into thecomponentsfolder of your project:bashcd your-project git clone https://github.com/thinger-io/IOTMP-ESPIDF.git components/iotmp-espidf git clone https://github.com/thinger-io/IOTMP-Embedded.git components/iotmp-embeddedTo keep them outside the project, set
EXTRA_COMPONENT_DIRSin the top-levelCMakeLists.txtto both folders instead.Then require the client from your
maincomponent. Requireesp-tlstoo, which the client headers use:main/CMakeLists.txtcmakeidf_component_register(SRCS "main.cpp" PRIV_REQUIRES iotmp-espidf esp-tls esp_wifi nvs_flash esp_timer)Write the application
The application connects to Wi-Fi, sets the credentials, declares the device resources and starts the client:
main/main.cppcpp#include <cstring> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "esp_timer.h" #include "nvs_flash.h" #include <thinger/iotmp/client.hpp> #define WIFI_SSID "wifi-network" #define WIFI_PASSWORD "wifi-password" static EventGroupHandle_t wifi_events; static const int WIFI_CONNECTED = BIT0; static void on_wifi_event(void* arg, esp_event_base_t base, int32_t id, void* data) { if (base == WIFI_EVENT && (id == WIFI_EVENT_STA_START || id == WIFI_EVENT_STA_DISCONNECTED)) { esp_wifi_connect(); // connect, and reconnect after a disconnection } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { xEventGroupSetBits(wifi_events, WIFI_CONNECTED); } } static void wifi_connect() { wifi_events = xEventGroupCreate(); esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_sta(); wifi_init_config_t init = WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(&init); esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &on_wifi_event, NULL, NULL); esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &on_wifi_event, NULL, NULL); wifi_config_t config = {}; strncpy((char*) config.sta.ssid, WIFI_SSID, sizeof(config.sta.ssid)); strncpy((char*) config.sta.password, WIFI_PASSWORD, sizeof(config.sta.password)); esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(WIFI_IF_STA, &config); esp_wifi_start(); xEventGroupWaitBits(wifi_events, WIFI_CONNECTED, pdFALSE, pdTRUE, portMAX_DELAY); } static thinger::iotmp::client thing; extern "C" void app_main(void) { if (nvs_flash_init() != ESP_OK) { nvs_flash_erase(); nvs_flash_init(); } wifi_connect(); thing.set_credentials("YOUR_USER", "YOUR_DEVICE", "YOUR_TOKEN"); // thing.set_host("YOUR_SERVER"); // only on a private instance // output resource: read by the console, the API or a Data Bucket thing["uptime"] = [](thinger::iotmp::output& out) { out["seconds"] = (uint32_t) (esp_timer_get_time() / 1000000); out["free_heap"] = (uint32_t) esp_get_free_heap_size(); }; // input resource: receives {"state": true} or {"state": false} thing["led"] = [](thinger::iotmp::input& in) { bool on = in["state"].get<bool>(); ESP_LOGI("app", "LED %s", on ? "on" : "off"); }; thing.start(); // connects and reconnects in its own task while (true) { vTaskDelay(pdMS_TO_TICKS(5 * 60 * 1000)); // store a reading in a Data Bucket every 5 minutes thinger::iotmp::json_t data; data["seconds"] = (uint32_t) (esp_timer_get_time() / 1000000); thing.write_bucket("uptime_data", data); } }YOUR_TOKENis the device credentials, not an access token.- Without
set_host(), the client connects to the free Cloud server. On a private instance, callthing.set_host("YOUR_SERVER"). - The client does not set up the network: your application connects to Wi-Fi (or Ethernet) before
thing.start(). - Create the
uptime_dataData Bucket in Data Buckets with From Device Write Call as Data Source before the first write.
Build and flash
bashidf.py set-target esp32 idf.py build flash monitorCheck the connection
Go to Devices: the device shows a green status dot. Open it and go to API, expand
uptimeand click Run to read its output. You can also runthinger device resource YOUR_DEVICE uptime.
Result
The ESP32 is online in Devices, its uptime device resource answers from the console, and the uptime_data Data Bucket gets a new reading every 5 minutes.
Define device resources
The client declares device resources with the type of their argument:
| Declaration | Kind |
|---|---|
thing["name"] = [](thinger::iotmp::output& out){ … }; | Output resource. |
thing["name"] = [](thinger::iotmp::input& in){ … }; | Input resource. |
thing["name"] = [](thinger::iotmp::input& in, thinger::iotmp::output& out){ … }; | Input and output. |
thing["name"] = [](){ … }; | Runs an action, without data. |
Read inputs by field with in["field"].get<bool>() (or get<float>(), get<int>()…), and fill outputs with out["field"] = value. thing["led"] << digitalPin(GPIO_NUM_2); controls a GPIO configured as an output.
The client can also call the platform from your code: thing.write_bucket(), thing.set_property(), thing.get_property(), thing.call_endpoint() and thing.stream(). These calls are safe from any task. See the Arduino coding guide for what each one does, and the IOTMP reference for the protocol.
Connection and reconnection
thing.start() runs the client in its own FreeRTOS task, named iotmp, and returns at once. The task connects, authenticates and reconnects on its own:
- After a failed attempt it waits 5 seconds, and doubles the wait on each new failure up to 60 seconds. A successful connection resets the wait.
thing.set_reconnect_timeout(base_ms, max_ms)changes both values. - It sends a keep-alive every 60 seconds;
thing.set_keepalive(seconds)changes it. thing.set_state_callback()receives the state changes (SOCKET_CONNECTING,AUTHENTICATED,DISCONNECTED,AUTH_FAILED…), for example to drive a status LED.thing.stop()closes the connection and stops the task.
Configuration
Run idf.py menuconfig and open Thinger IOTMP Client:
| Option | Default | Description |
|---|---|---|
CONFIG_THINGER_IOTMP_TLS | Enabled | Connect over TLS (port 25206). Disable it for plain TCP (port 25204). |
CONFIG_THINGER_IOTMP_STACK_SIZE | 8192 | Stack size of the client task, in bytes. |
CONFIG_THINGER_IOTMP_PRIORITY | 5 | Priority of the client task. |
With TLS, the client checks the server certificate with the ESP-IDF certificate bundle (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE, enabled by default). The network must allow outgoing connections to the port in use.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
client.hpp cannot find esp_tls.h when building | esp-tls is missing from the requirements of your component. | Add esp-tls to REQUIRES or PRIV_REQUIRES, as in step 2. |
The state goes to AUTH_FAILED | The credentials do not match the device. | Check the username, the device ID and the device credentials. |
The state stays in SOCKET_CONNECTING | The server cannot be reached. | Check the network, the host and that the port is open. |
More cases in Troubleshooting.