Skip to content

Devices

Coding guide

Write Arduino firmware for Thinger.io - device resources, pins, Data Buckets, properties, endpoints and connection handling.

Guide15 min read

This guide shows how to write the firmware of a device that uses the Thinger.io Arduino library: expose device resources, send data to a Data Bucket, read and write properties, call endpoints and keep the connection healthy. The examples use an ESP32, but the code is the same for every supported board; only the include and the class change.

Before you start ​

  • A sketch that already connects, as in Arduino.
  • The device ID, the device credentials and your username. YOUR_SERVER is iot.thinger.io on the free Cloud account, or your own domain on a private instance.

For the full list of functions, see the Arduino library reference.

CALL DEVICE RESOURCESONE CONNECTIONTHE DEVICE CALLS THE PLATFORMConsoleREST APIDashboardsOther devicesAI agentsDevicedevice resources declared in setup()thing["fan"] << …thing["temp"] >> …thing["sum"] = (in, out)thing["restart"] = ()Input: receives a valueOutput: sends a valueInput and outputNo data: runs an actionwrite_bucket()get_property() / set_property()call_endpoint()call_device()stream()Data BucketPropertiesEndpoint: email, HTTP…Another deviceDashboard, or Stream by Device bucketBoth directions share the connection that thing.handle() keeps open.
Device resources answer the platform; the device can also call the platform

Structure of a sketch ​

Every sketch has the same shape: create the thing object with your credentials, declare the device resources in setup(), and call thing.handle() in loop().

main.cpp
cpp
#define THINGER_SERIAL_DEBUG

#include <ThingerESP32.h>

ThingerESP32 thing("YOUR_USER", "YOUR_DEVICE", "YOUR_TOKEN");

void setup() {
  Serial.begin(115200);
  thing.add_wifi("wifi-network", "wifi-password");

  // declare device resources here
}

void loop() {
  thing.handle();
}

thing.handle() keeps the connection alive and answers the requests of the platform. Call it as often as possible: avoid delay() and long loops in loop(), or the device stops answering and the platform marks it as disconnected. Use millis() to run code at intervals, as in Send data to a Data Bucket.

The examples of the library (File > Examples > thinger.io) take the username, device ID and device credentials from a second file, arduino_secrets.h, instead of writing them in the sketch. Fill in that file when you start from an example: see Start from a library example.

Define device resources ​

A device resource is a function of the device that the platform can call: from the console, the REST API, a dashboard or another device. Declare each one in setup() with a name and an operator that sets its direction.

SyntaxKindUse it for
thing["name"] << [](pson& in){ … };Input resourceReceive data: switch a relay, change a setting.
thing["name"] >> [](pson& out){ … };Output resourceSend data: a sensor reading.
thing["name"] = [](pson& in, pson& out){ … };Input and outputReceive data and return a result.
thing["name"] = [](){ … };No dataRun an action: reboot, start a measurement.

pson holds any JSON-like value: a number, a boolean, a string or an object with named fields.

Do not capture local variables by reference

The library keeps the functions of resources, of the state listener and of OTA, and runs them later, when setup() has already returned. A function that captures with [&] a local variable of setup(), or of a function called from it, reads memory that is no longer valid, and the device fails at random. Use global variables, or capture by value with [=].

Input resources ​

An input resource receives a value from the platform. Read it from in:

cpp
bool fan = false;

void setup() {
  // ...
  thing["fan"] << [](pson& in) {
    if (in.is_empty()) {
      in = fan;        // report the current state
    } else {
      fan = in;        // apply the new state
    }
  };
}

When the console opens a device resource, it first reads its current value with an empty in. Filling in when it is empty, as above, shows the current state instead of a default value.

To receive several values, use named fields:

cpp
float targetTemperature = 21.0;
int targetHumidity = 50;

void setup() {
  // ...
  thing["setpoint"] << [](pson& in) {
    if (in.is_empty()) {
      in["temperature"] = targetTemperature;
      in["humidity"] = targetHumidity;
    } else {
      targetTemperature = in["temperature"];
      targetHumidity = in["humidity"];
    }
  };
}

Output resources ​

An output resource sends a value each time the platform reads it. Fill out with a single value or with named fields:

cpp
void setup() {
  // ...
  thing["uptime"] >> [](pson& out) {
    out = millis() / 1000;
  };

  thing["status"] >> [](pson& out) {
    out["rssi"] = WiFi.RSSI();
    out["heap"] = ESP.getFreeHeap();
  };
}

Input and output resources ​

A resource can take an input and return a result in the same call:

cpp
void setup() {
  // ...
  thing["sum"] = [](pson& in, pson& out) {
    out["result"] = (int) in["a"] + (int) in["b"];
  };
}

Resources without data ​

A resource without in or out runs an action when it is called:

cpp
void setup() {
  // ...
  thing["restart"] = []() {
    ESP.restart();
  };
}

Resources in the console ​

The API tab of the device lists its device resources once it is connected. An input shows a field to send, an output shows the value it returns, a resource with both shows each side, and a resource without data shows only Run:

Device API with the fan resource as an on/off switch, restart with only Run, status with its heap and rssi output, and sum with the inputs a 2 and b 3 and the output result 5 after Run
The fan, restart, status and sum resources of this section in the API tab

Control pins and variables ​

The library has shortcuts for the most common resources:

ShortcutKindWhat it does
digitalPin(pin)InputSets a digital output pin on or off, and reports its state.
invertedDigitalPin(pin)InputThe same with the logic inverted: on sets the pin LOW. For relay modules that switch on with a low level. Only on AVR boards (such as the Arduino UNO) and ESP8266.
analogPin(pin)InputWrites an analog (PWM) value to a pin. Not available on ESP32.
outputValue(value)OutputSends the value of an expression or variable.
outputString(value)OutputThe same, for a text.
inputValue(variable)InputStores the value received in a variable, and reports it.
inputValue(variable, code)InputThe same, and runs code after each change.
servo(object)InputMoves a servo of the Arduino Servo library to the angle received, and reports its angle.
cpp
#define RELAY_PIN 26

int interval = 60;

void setup() {
  // ...
  pinMode(RELAY_PIN, OUTPUT);

  thing["relay"] << digitalPin(RELAY_PIN);
  thing["light"] >> outputValue(analogRead(34));
  thing["interval"] << inputValue(interval, {
    Serial.println("Interval changed");
  });
}

The pin passed to digitalPin must be a number or a #define, not a variable. Configure the pin as an output with pinMode before you use it.

On AVR boards and ESP8266, digitalPin reads the pin to report its state. On the other boards (ESP32, SAMD, Mbed and Renesas boards) it reports the last value set from the platform, which starts as off.

Many relay modules switch on when their input is LOW. On AVR boards and ESP8266, invertedDigitalPin keeps the resource "on" when the relay is on:

main.cpp
cpp
#include <ThingerESP8266.h>

#define RELAY_PIN 5  // D1 on NodeMCU boards

ThingerESP8266 thing("YOUR_USER", "YOUR_DEVICE", "YOUR_TOKEN");

void setup() {
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, HIGH);  // relay off at start

  thing.add_wifi("wifi-network", "wifi-password");

  thing["relay"] << invertedDigitalPin(RELAY_PIN);
}

void loop() {
  thing.handle();
}

On the other boards, write an input resource that inverts the value:

cpp
bool relayOn = false;

void setup() {
  // ...
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, HIGH);  // relay off at start

  thing["relay"] << [](pson& in) {
    if (in.is_empty()) {
      in = relayOn;
    } else {
      relayOn = in;
      digitalWrite(RELAY_PIN, relayOn ? LOW : HIGH);
    }
  };
}

Send data to a Data Bucket ​

A Data Bucket stores the readings of a device as a time series. Create the bucket in Data Buckets first. Its Data Source sets who decides when a reading is stored:

Data SourceRefresh ModeWho decidesWhat the sketch needs
From Device ResourceSampling IntervalThe platform reads the output resource at a fixed interval.Only the output resource.
From Device ResourceStream by DeviceThe device, when it calls thing.stream().The output resource and a thing.stream() call.
From Device Write Call—The device, when it calls thing.write_bucket().A thing.write_bucket() call with the bucket ID.
DEVICEWHO STARTS ITDATA BUCKETFrom Device Resource · Sampling Intervalthing["env"] >> …nothing elseThe platform reads it every N secondsoutput storedFrom Device Resource · Stream by Devicething["env"] >> …thing.stream("env")The device sends it when it decidesthe bucket listens and stores each valueFrom Device Write Callwrite_bucket( "bucket", "env")The device writes to a bucket IDa device resource or a pson valueData BucketData BucketData Bucket
Three ways a device fills a Data Bucket

Write to a bucket ​

With From Device Write Call, call thing.write_bucket() with the bucket ID and a device resource whose output is the data to store:

cpp
unsigned long lastWrite = 0;

void setup() {
  // ...
  thing["environment"] >> [](pson& out) {
    out["temperature"] = temperatureRead();
    out["uptime"] = millis() / 1000;
  };
}

void loop() {
  thing.handle();

  // store a reading every 5 minutes
  if (millis() - lastWrite >= 5 * 60 * 1000UL) {
    lastWrite = millis();
    thing.write_bucket("environment_data", "environment");
  }
}

You can also write a pson value directly with thing.write_bucket("environment_data", data). A third argument true waits for the platform to confirm the write, and the call returns false if it failed. See Data Buckets.

Warning

Do not write in every iteration of loop(). The platform limits how often a device can write (see Plans and limits), and it disables a device whose calls keep failing: see Call the platform at a safe rate.

Store a value when the device decides ​

With From Device Resource and the Stream by Device refresh mode, the bucket listens to an output resource and stores a reading each time the sketch calls thing.stream(). The device decides when to store, without knowing the bucket ID. For example, store the position only while the vehicle moves:

main.cpp
cpp
#include <ThingerESP32.h>

ThingerESP32 thing("YOUR_USER", "YOUR_DEVICE", "YOUR_TOKEN");

float latitude = 0, longitude = 0;
bool requiresRecording = false;  // set it from your GPS code
unsigned long lastRecord = 0;

void setup() {
  thing.add_wifi("wifi-network", "wifi-password");

  thing["location"] >> [](pson& out) {
    out["lat"] = latitude;
    out["lon"] = longitude;
  };
}

void loop() {
  thing.handle();

  // update latitude, longitude and requiresRecording from the GPS here

  // store the position at most every 10 seconds while it changes
  if (requiresRecording && millis() - lastRecord >= 10000UL) {
    lastRecord = millis();
    thing.stream("location");
  }
}

In the bucket, choose the device in Select Device, location in Select Resource and Stream by Device in Refresh Mode.

Read and write device properties ​

Properties are named JSON values stored on the device in the platform. They keep configuration and state across restarts, and you can edit them in the console. Read a property with thing.get_property() and write it with thing.set_property():

cpp
int reportInterval = 60;

void loadConfig() {
  pson config;
  if (thing.get_property("config", config)) {
    reportInterval = config["interval"];
  }
}

void saveConfig() {
  pson config;
  config["interval"] = reportInterval;
  thing.set_property("config", config, true);  // true: wait for confirmation
}

Both calls need the device to be connected and return false when they fail. See Properties.

Call an endpoint ​

An endpoint is an integration configured in the platform: an HTTP request, an email, a Telegram message. Create it in Endpoints and call it by its ID:

cpp
void sendAlert(float temperature) {
  pson data;
  data["temperature"] = temperature;
  thing.call_endpoint("high_temperature", data);
}
CallData sent to the endpoint
thing.call_endpoint("id")None.
thing.call_endpoint("id", data)A pson value.
thing.call_endpoint("id", thing["resource"])The output of a device resource.
thing.call_endpoint("id", "resource")The same, by the name of the device resource.

A last argument true waits for the platform to confirm the call. See Endpoints.

Send an email once per event ​

Call an endpoint when something happens, not while it lasts. This sketch calls an email endpoint once when the level of a tank crosses a limit, and only again after the level drops below a lower one:

main.cpp
cpp
#include <ThingerESP32.h>

#define LEVEL_PIN 34
#define HIGH_LEVEL 3000   // reading that sends the email
#define REARM_LEVEL 2800  // reading that allows a new email

ThingerESP32 thing("YOUR_USER", "YOUR_DEVICE", "YOUR_TOKEN");

bool alertSent = false;

void setup() {
  thing.add_wifi("wifi-network", "wifi-password");

  thing["tank"] >> [](pson& out) {
    out["level"] = analogRead(LEVEL_PIN);
  };
}

void loop() {
  thing.handle();

  int level = analogRead(LEVEL_PIN);
  if (!alertSent && level > HIGH_LEVEL) {
    thing.call_endpoint("tank_alert", thing["tank"]);
    alertSent = true;
  } else if (alertSent && level < REARM_LEVEL) {
    alertSent = false;
  }
}

The email endpoint tank_alert uses the fields of the data in its subject and body, with double braces: {{level}} is the level field of the tank output.

text
Subject: Tank level alert
Body:    The tank level is {{level}}, above the limit.

Call another device ​

How a device calls a device resource of another device depends on the account of the other device:

The other device is inCall
The same accountthing.call_device(), directly.
Another accountthing.call_endpoint(), to an endpoint that calls the device of the other account.

The other device must be connected. Call it on an event or at an interval, as in Call the platform at a safe rate.

Devices of the same account ​

The device that receives the call, YOUR_DEVICE, declares the device resources. The caller uses the same names and, to send data, the same field names:

cpp
int interval = 60;
float limit = 25.0;

void setup() {
  // ...
  // called without data
  thing["blink"] = []() {
    Serial.println("Someone is calling me");
  };

  // called with data: read the fields the caller sends
  thing["setpoint"] << [](pson& in) {
    if (in.is_empty()) {
      in["interval"] = interval;
      in["limit"] = limit;
    } else {
      interval = in["interval"];
      limit = in["limit"];
    }
  };
}
cpp
void setup() {
  // ...
  thing["config"] >> [](pson& out) {
    out["interval"] = 30;
    out["limit"] = 22.5;
  };
}

void callOtherDevice() {
  // without data
  thing.call_device("YOUR_DEVICE", "blink");

  // with a pson value
  pson data;
  data["interval"] = 120;
  data["limit"] = 27.5;
  thing.call_device("YOUR_DEVICE", "setpoint", data);

  // with the output of a device resource of this device
  thing.call_device("YOUR_DEVICE", "setpoint", thing["config"]);
}

A last argument true waits for the platform to confirm the call, and the call returns false if it failed, for example because the other device is not connected.

Devices of different accounts ​

In the account of the calling device, create an endpoint that calls the device resource in the other account. You need an access token of the other account that allows calling that device resource.

  • On the free Cloud server: use the Thinger.io Device Call endpoint type, and fill in Device Owner Username, Device Identifier, Resource Name and Device Access Token with the values of the other account.
  • On any server: use an HTTP Request endpoint with the POST method, the URL https://YOUR_SERVER/v3/users/{user}/devices/{device}/resources/{resource} with the username, device ID and resource of the other account, a header Authorization with the value Bearer followed by the access token, and a JSON body.

The device calls the endpoint as any other, with or without data:

cpp
pson data;
data["on"] = true;
thing.call_endpoint("neighbor_lamp", data);

Call the platform at a safe rate ​

write_bucket, call_endpoint, call_device, set_property and stream each send a message. Too many calls use the data of a cellular plan, fill an inbox with emails or use up the quota of a third-party service. Call them when something changes, or at an interval measured with millis(); never in every iteration of loop().

The platform disables devices whose calls keep failing

If 100 Data Bucket writes or endpoint calls of a device fail within about a minute, the platform disables the device, closes its connection and sends a Device Disabled email to the account. A call fails when, for example, the bucket or the endpoint does not exist or is disabled, or the device goes over the limits of the account. The device cannot connect again until you fix the sketch and enable the device: see Troubleshooting.

Stream a resource in real time ​

thing.stream() pushes the current value of an output resource to whatever listens to it: an open dashboard or console page, or a Data Bucket in Stream by Device mode, which stores each value (see Store a value when the device decides). When nothing listens, it sends nothing and returns false.

Call it with the name of the device resource, thing.stream("level"), or with the resource, thing.stream(thing["level"]). Stream when the value changes enough to matter:

main.cpp
cpp
#include <ThingerESP32.h>

#define LEVEL_PIN 34

ThingerESP32 thing("YOUR_USER", "YOUR_DEVICE", "YOUR_TOKEN");

int lastSent = 0;

void setup() {
  thing.add_wifi("wifi-network", "wifi-password");

  thing["level"] >> [](pson& out) {
    out = analogRead(LEVEL_PIN);
  };
}

void loop() {
  thing.handle();

  // stream only when the reading changes by more than 50
  int level = analogRead(LEVEL_PIN);
  if (abs(level - lastSent) > 50) {
    lastSent = level;
    thing.stream(thing["level"]);
  }
}

Manage the connection ​

The library connects to the network and to the platform from thing.handle(), and reconnects on its own when the connection drops. While the device is offline, thing.handle() does not hold loop(), so the sketch keeps running: it can read its sensors, or show the connection state on a display with thing.get_state().

  • Wi-Fi. Most Wi-Fi boards join the network step by step, one step on each thing.handle(), with the state NETWORK_CONNECTING in the meantime. They give up after 30 seconds without joining the network, and after 30 more without an IP address. With several networks, the board scans them for a few seconds inside thing.handle().

  • Other networks. On Ethernet, on cellular modems and with WebConfig, ESP-Touch, the CC3000 module, the ESP8266 as Wi-Fi modem and the LinkIt ONE, thing.handle() waits while the board joins the network.

  • Server. Opening the TLS connection and authenticating take a few seconds on every attempt, inside thing.handle().

  • Retries. Between two attempts, thing.handle() returns at once. After a network failure it tries again in 5 seconds. After a failure with the server (socket, TLS or authentication), it waits 5 seconds and doubles the wait on every new failure, up to 30 seconds, with a random margin of up to 25 %, so many devices do not reconnect at the same time after a server restart. The wait starts again from 5 seconds once the device connects. RECONNECTION_TIMEOUT and THINGER_RECONNECTION_MAX_TIMEOUT change these times: see Reconnection.

  • thing.add_wifi(ssid, password) adds a Wi-Fi network. Some boards accept several, as in Connect to several Wi-Fi networks. To let end users set the network without recompiling, see WebConfig.

  • thing.is_connected() returns true while the device is connected.

  • thing.get_state() returns the last connection state, one of the table below: ThingerClient::THINGER_AUTHENTICATED means the device is online, and ThingerClient::SOCKET_DISCONNECTED that it is offline.

  • thing.get_error() returns why the network connection failed: see Why the network connection failed.

  • #define THINGER_SERVER "YOUR_SERVER", before the include, connects to a private instance. Without it, the device connects to the free Cloud account. If the certificate of your instance comes from its own authority, define its root certificate too, as in Server and TLS.

To react to connection changes, register a state listener:

cpp
void setup() {
  // ...
  thing.set_state_listener([](ThingerClient::THINGER_STATE state) {
    switch (state) {
      case ThingerClient::THINGER_AUTHENTICATED:
        Serial.println("Connected to Thinger.io");
        break;
      case ThingerClient::THINGER_AUTH_FAILED:
        Serial.println("Check the device credentials");
        break;
      case ThingerClient::SOCKET_DISCONNECTED:
        Serial.println("Disconnected");
        break;
      default:
        break;
    }
  });
}
StateMeaning
NETWORK_CONNECTING, NETWORK_CONNECTED, NETWORK_CONNECT_ERRORJoining the network (Wi-Fi, Ethernet or cellular).
SOCKET_CONNECTING, SOCKET_CONNECTED, SOCKET_CONNECTION_ERROROpening the connection to the server.
SOCKET_DISCONNECTED, SOCKET_TIMEOUT, SOCKET_ERRORThe connection to the server is lost.
THINGER_AUTHENTICATING, THINGER_AUTHENTICATED, THINGER_AUTH_FAILEDChecking the device credentials.
THINGER_STOP_REQUESTThe sketch closed the connection.

Why the network connection failed ​

After a NETWORK_CONNECT_ERROR state, thing.get_error() tells the reason. Read it in the state listener or at any time; once the network is connected, it returns ThingerClient::ERROR_NONE.

cpp
void setup() {
  // ...
  thing.set_state_listener([](ThingerClient::THINGER_STATE state) {
    if (state == ThingerClient::NETWORK_CONNECT_ERROR) {
      switch (thing.get_error()) {
        case ThingerClient::NETWORK_AUTH_ERROR:
          Serial.println("Wrong Wi-Fi password");
          break;
        case ThingerClient::NETWORK_NOT_REGISTERED:
          Serial.println("No cellular coverage");
          break;
        default:
          break;
      }
    }
  });
}
ErrorMeaningBoards
ERROR_NONEThe network is connected.All
ERROR_UNKNOWNThe board does not report the reason.All
NETWORK_NOT_CONFIGUREDNo Wi-Fi network or APN is set. With WebConfig, the configuration page closed without a Wi-Fi network.Wi-Fi and cellular
NETWORK_NOT_FOUNDThe Wi-Fi network is not available.Wi-Fi
NETWORK_AUTH_ERRORThe Wi-Fi network rejected the connection, usually for a wrong password.Wi-Fi
NETWORK_NO_IPThe board joined the Wi-Fi network but got no IP address.Wi-Fi
MODEM_NOT_RESPONDINGThe modem does not answer.ThingerTinyGSM
MODEM_SIM_ERRORThe SIM card cannot be used, for example for a wrong PIN.ThingerTinyGSM
NETWORK_NOT_REGISTEREDThe modem cannot register in the network, for example for lack of coverage.ThingerTinyGSM, ThingerNBCore, ThingerBC66
NETWORK_REGISTRATION_DENIEDThe operator rejected the registration, for example because the SIM card is disabled.ThingerNBCore, ThingerBC66
NETWORK_APN_ERRORThe modem is registered, but the data connection does not open: check the APN.ThingerTinyGSM, ThingerMKRNB, ThingerMKRGSM

Connect to several Wi-Fi networks ​

On the ESP32, the ESP8266 and the Raspberry Pi Pico W and Pico 2 W, call thing.add_wifi() once per network. With two or more networks, the board scans and joins the strongest one it knows, also when it reconnects:

cpp
void setup() {
  thing.add_wifi("home-network", "home-password");
  thing.add_wifi("office-network", "office-password");

  // ...
}
  • With a single network, the board joins it without scanning. A hidden network, one that does not broadcast its name, only works this way: as the only network of the sketch.
  • On the other boards, the library keeps one network: each call replaces the previous one, and the board joins the last one added.
  • With debug output on, the serial monitor shows Connecting to the strongest known network with the number of networks, and then the name of the network it joined.

Connect to a WPA2-Enterprise network ​

On the ESP32, thing.add_wifi_enterprise(ssid, username, password, identity, ca_pem) adds a WPA2-Enterprise network that uses PEAP with EAP-MSCHAPv2, such as eduroam. It needs version 3 of the esp32 board package: with version 2, the function does not exist.

  • identity is the outer identity, the name the network sees before the credentials are checked. Leave it out and the board uses the username. Many networks accept an anonymous one; on eduroam it is usually anonymous@ followed by the domain of your institution.
  • ca_pem is the certificate, in PEM format, of the authority that signs the network's authentication server. Your organization provides it; on eduroam, each institution publishes its own. With it, the board checks the server before it sends the username and password.
cpp
const char EDUROAM_CA[] = R"(-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----)";

void setup() {
  thing.add_wifi_enterprise("eduroam", "user@example.com", "password", "anonymous@example.com", EDUROAM_CA);

  // ...
}

You can add it next to other networks with add_wifi(). A network with a certificate is joined first, on its own; if the board cannot join it in 30 seconds, it tries the other networks and joins the strongest of them. Only one network can carry a certificate: a second network added with one is joined without checking its server.

Without a certificate, the board does not verify the authentication server

If you leave out ca_pem, a rogue access point with the same network name could capture the username and password. Pass the certificate of your network, and use an account for the device, not a personal one.

Debug output ​

Add #define THINGER_SERIAL_DEBUG as the first line of the sketch, and open the serial monitor at 115200 baud. The library prints each connection step:

text
[NETWORK] Starting connection...
[NETWORK] Connecting to network wifi-network
[NETWORK] Connected to WiFi!
[NETWORK] Getting IP Address...
[NETWORK] Got IP Address: 192.168.1.40
[NETWORK] Connected!
[_SOCKET] Connecting to iot.thinger.io:25202...
[_SOCKET] Using secure TLS/SSL connection: yes
[_SOCKET] Connected!
[THINGER] Authenticating. User: YOUR_USER Device: YOUR_DEVICE
[THINGER] Authenticated

After a failed attempt, the monitor shows how long the library waits before the next one, in milliseconds:

text
[THINGER] Retrying connection in (ms): 10641

If the device does not reach Authenticated, see Troubleshooting.

Result ​

The device exposes its device resources, stores data in a Data Bucket, keeps its configuration in properties and calls endpoints when something happens. In Devices, open the device and its API tab to call each device resource.

Next steps ​