July 6, 2026 Hunter McGuire

CircuitPython to Tendrl: pure MQTT, no SDK required

The Tendrl client is MicroPython-only. CircuitPython boards don't lose anything: the protocol underneath is just MQTT and JSON.

The tendrl package installs via mip, which is a MicroPython-only tool. CircuitPython doesn't have it, and won't. If you're on an Adafruit Feather, QT Py, or any other CircuitPython board, that can look like a dead end.

It isn't one. Contact doesn't require the SDK. The SDK is a convenience wrapper around a plain MQTT broker with a documented JSON message contract. Anything that speaks MQTT can talk to it, including CircuitPython's own adafruit_minimqtt library. This guide builds a weather sensor that publishes readings, listens for commands, and reports its state, entirely without the client.

What you'll build

Prerequisites

Step 1: Create an entity

In the Tendrl dashboard, go to Entities → Add Entity and name it circuitpython-weather. Open Connection Instructions and copy three things: the API Key ID, the API Key secret, and the entity's resourcePath (form: account:region:entity:name). All three go into settings.toml in the next step; see the full field reference in the MQTT protocol guide.

Step 2: Install the MQTT library

CircuitPython has no SDK to install for Tendrl, just the general-purpose MQTT client:

Terminal
circup install adafruit_minimqtt adafruit_bme280

Step 3: Store credentials in settings.toml

CircuitPython loads settings.toml automatically and exposes it through os.getenv, so credentials never sit in code.py:

settings.toml
CIRCUITPY_WIFI_SSID = "your-wifi"
CIRCUITPY_WIFI_PASSWORD = "your-password"
TENDRL_API_KEY_ID = "your-api-key-id"
TENDRL_API_KEY_SECRET = "your-api-key-secret"
TENDRL_ACCOUNT = "your-account-number"

Step 4: Write code.py

This connects over TLS on port 443 (same as every other Tendrl client, no separate port to open), subscribes to the entity's messages topic for inbound commands, reports state once at boot, and publishes sensor readings on a loop:

code.py
import os
import ssl
import json
import time
import wifi
import socketpool
import board
import busio
import adafruit_bme280.advanced as adafruit_bme280
import adafruit_minimqtt.adafruit_minimqtt as MQTT

API_KEY_ID = os.getenv("TENDRL_API_KEY_ID")
API_KEY_SECRET = os.getenv("TENDRL_API_KEY_SECRET")
ACCOUNT = os.getenv("TENDRL_ACCOUNT")
REGION = "us-1"
RESOURCE_PATH = f"{ACCOUNT}:{REGION}:entity:circuitpython-weather"

PUB_TOPIC = f"{ACCOUNT}/{REGION}/{API_KEY_ID}/publish"
SUB_TOPIC = f"{ACCOUNT}/{REGION}/{API_KEY_ID}/messages"

# Sensor
i2c = busio.I2C(board.SCL, board.SDA)
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

# Wi-Fi
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
pool = socketpool.SocketPool(wifi.radio)

interval_s = 30  # changeable at runtime, see on_message below

def on_connect(mqtt_client, userdata, flags, rc):
    print("Connected to Tendrl")
    mqtt_client.subscribe(SUB_TOPIC, qos=1)

def on_message(mqtt_client, topic, message):
    global interval_s
    data = json.loads(message).get("data", {})
    if "set_interval_s" in data:
        interval_s = int(data["set_interval_s"])
        print(f"Interval updated to {interval_s}s")

mqtt_client = MQTT.MQTT(
    broker="mqtt.tendrl.com",
    port=443,
    username=API_KEY_ID,
    password=API_KEY_SECRET,
    client_id=RESOURCE_PATH,
    socket_pool=pool,
    ssl_context=ssl.create_default_context(),
)
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
mqtt_client.connect()

# Report firmware/mode once at boot via the state table
mqtt_client.publish(
    PUB_TOPIC,
    json.dumps({"msg_type": "state_new", "data": {"firmware": "1.0.0", "mode": "active"}}),
    qos=1,
)

last_sent = 0
while True:
    mqtt_client.loop(timeout=1)
    now = time.monotonic()
    if now - last_sent >= interval_s:
        message = {
            "msg_type": "publish",
            "data": {
                "temperature": round(bme280.temperature, 2),
                "humidity": round(bme280.relative_humidity, 2),
                "pressure": round(bme280.pressure, 2),
            },
            "context": {"tags": ["weather", "circuitpython"]},
        }
        mqtt_client.publish(PUB_TOPIC, json.dumps(message), qos=1)
        last_sent = now

Power it on. The board connects, reports its firmware state once, then publishes a weather reading every 30 seconds, visible in the Tendrl dashboard under the entity, same as any SDK-based client.

Step 5: Change the interval remotely

Because the device subscribes to its own messages topic, you can reach it without touching the board. From the dashboard's message composer (or a REST call), send:

Send to entity
{
  "msg_type": "publish",
  "data": { "set_interval_s": 10 }
}

The board's on_message handler picks it up on the next loop() call and starts reporting every 10 seconds. No reflash, no reboot.

Why this works without an SDK

Everything the MicroPython client does under the hood (TLS on port 443, the {account}/{region}/{apiKeyId}/... topic scheme, the msg_type/data envelope, QoS 1 delivery) is just documented protocol, not private wire format. The full reference, including the state topic and validation status fields, is in the MQTT protocol guide. Any board or language with an MQTT client and a JSON encoder can be a first-class Tendrl entity.

What's next

Coming soon MQTT protocol guide