Identity parsing
Parse the identity JSON issued by the Thalovant API, with the same field aliases the Node and Go SDKs accept.
Use the embedded C library when your client is a microcontroller or small device, such as ESP32, Zephyr, bare-metal, or a Linux single-board computer.
This is a protocol library, not a full SDK. It is transport-agnostic: you bring your own MQTT client or WebSocket client and your own TLS stack, and the library provides everything protocol-specific. It is pure C99 with zero external dependencies, and the core paths never allocate; every function writes into caller-provided buffers.
Source and releases: thalovant-embedded-c on GitHub.
Identity parsing
Parse the identity JSON issued by the Thalovant API, with the same field aliases the Node and Go SDKs accept.
MQTT topics
Derive per-client MQTT topics, connection endpoints, ports, and client IDs, byte-for-byte identical to the Node SDK.
Payload encryption
Self-contained AES-128-GCM with constant-time tag verification, validated against NIST vectors.
Wire framing
Build and parse hub wire frames and encrypted envelopes, plus helpers for the ask request and reply loop.
| You provide | The library provides |
|---|---|
| MQTT and/or WebSocket client | Topics, endpoints, frame and envelope codecs |
| TLS stack | TLS flag plus scheme and port parsing |
| Random number generator | Nothing; nonces are always caller-supplied |
| Event loop and timers | Frame classifier and ask-loop semantics |
| Identity JSON storage | Identity parser |
The library has no control-plane client. Create the client identity through the dashboard, the API, or another SDK, then store the identity JSON on the device.
Vendor the library or fetch it by an immutable release tag. The current tag is v0.1.0.
git submodule add https://github.com/thalovant/thalovant-embedded-c.git \ third_party/thalovant-embedded-cgit -C third_party/thalovant-embedded-c checkout v0.1.0CMake FetchContent, ESP-IDF component references, and Zephyr west manifests work the same way with the v0.1.0 tag. Every GitHub release also carries a reproducible source archive, a CycloneDX SBOM, and a SHA256SUMS file, attested with GitHub Actions provenance.
Building needs only a C99 compiler:
make # build/libthalovant.amake test # host-side, offline test suiteThis MQTT sketch parses an identity, derives topics and the payload key, sends one utterance, and classifies the replies. Your MQTT client owns the connection.
#include "thalovant/thalovant.h"
thalovant_identity identity;thalovant_identity_parse(identity_json, identity_len, &identity);
thalovant_mqtt_topics topics;thalovant_mqtt_topics_derive(&identity, &topics);
uint8_t key[16];thalovant_crypto_runtime_key(identity.crypto_key, key);
/* connect your MQTT client to the derived endpoint... *//* subscribe topics.outbound; publish "online" retained on topics.status */
/* send an utterance */char frame[1024];thalovant_ask_request ask = { "what time is it", "en-us", "sess-1", identity.site_id, "req-1" };thalovant_ask_build_frame(&ask, frame, sizeof(frame));
uint8_t nonce[16]; /* fill from your RNG — never reuse */uint8_t sealed[1100];size_t sealed_len;thalovant_envelope_encrypt_binary(key, nonce, (uint8_t *)frame, strlen(frame), sealed, sizeof(sealed), &sealed_len);/* publish sealed on topics.inbound ... */
/* classify replies arriving on topics.outbound */thalovant_ask_event event;thalovant_ask_classify(plaintext, plaintext_len, "req-1", &event);if (event.kind == THALOVANT_ASK_SPEAK) { /* speak event.text */ }The repository includes full walkthroughs for ESP32 with MQTT and for Linux with a WebSocket client in its docs/ directory.
A connected client can list every intent its hub answers, per language, with the sentences a person says to reach each one, over its own session and with no control-plane token. See What Can My Hub Be Asked? for what the hub returns, which message types the connection must be allowed to publish, and what a refusal or a failed listing looks like.
Since release v0.2.0 the library carries the protocol side of it, in the same style as thalovant_ask_*. thalovant_intent_list_build_frame and thalovant_intent_describe_build_frame build the ovos.intent.list and ovos.intent.describe frames, and thalovant_intent_classify reads a reply into a thalovant_intent_event. Your transport seals and sends the frames, feeds the replies back, and correlates them by the request id you set.
struct inventory_query { const char *request_id; bool answered; };
static bool on_row(const thalovant_intent_registration *row, void *user){ printf("%s:%s (%s) %s\n", row->skill_id, row->intent_name, row->lang, row->method); return true; /* false stops the walk */}
struct inventory_query query = { "req-2", false };thalovant_intent_list_request list = { "en-us", "sess-1", identity.site_id, query.request_id, false };if (thalovant_intent_list_build_frame(&list, frame, sizeof(frame)) < 0) { /* THALOVANT_ERR_NOMEM: frame[] too small; nothing was built to send */}/* seal and publish the frame, then classify each reply that arrives */
thalovant_intent_event reply;thalovant_intent_classify(plaintext, plaintext_len, query.request_id, &reply);switch (reply.kind) {case THALOVANT_INTENT_LIST_RESPONSE: if (query.answered) break; /* the hub delivers every reply twice */ query.answered = true; thalovant_intent_list_rows(&reply, on_row, NULL); break;case THALOVANT_INTENT_POLICY_DENIED: /* reply.denied_type names the query */ break;default: break; /* THALOVANT_INTENT_IGNORE */}The walkers deliver one item at a time through a callback, so a manifest of any size is read in bounded memory: thalovant_intent_list_rows for the registrations, thalovant_intent_definitions for a describe reply, and thalovant_intent_samples for the sentences behind one definition. thalovant_intent_same_language folds language tags the way the other SDKs do, so fr-fr and fr_FR match. A row’s method is template or keyword, which the full SDKs expose as the engines padatious and adapt.
A refusal arrives as THALOVANT_INTENT_POLICY_DENIED with the refused type in reply.denied_type. A listing the hub accepted and failed is a different case: since v0.3.0, thalovant_intent_list_rows returns THALOVANT_ERR_HUB_REFUSED with the hub’s own words in reply.error, because a failed listing is not an empty hub. thalovant_intent_definitions returns zero definitions for the same answer to a describe, which is a real one: the hub does not know that registration. Correlation, retries, and the result model are yours to own; see What Can My Hub Be Asked? for the shape the full SDKs build from the same replies.
Use Python, Node.js, Go, or Rust when the device can run a full runtime. Those SDKs own the transport, reconnect logic, and control-plane calls for you. Use MQTT for broker details and topic scope.
| Symptom | Check |
|---|---|
| Identity parse fails | Confirm the stored JSON is the untouched identity payload from the API or dashboard download. |
| Broker rejects the connection | Confirm the derived endpoint, port, and TLS flag, and use the per-client broker credentials from the identity. |
| Replies never classify | Confirm the subscribe topic is topics.outbound and the request ID matches the one sent in the ask frame. |
| Decrypt fails | Confirm the payload key comes from thalovant_crypto_runtime_key and the nonce is read from the received envelope. |