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
- A CircuitPython board with a BME280 publishing temperature, humidity, and pressure to Tendrl
- A remote-control channel: send a message to the device's
messagestopic to change its reporting interval live, no redeploy - A one-time state report so the dashboard shows firmware version and mode
Prerequisites
- A CircuitPython board with native Wi-Fi (ESP32-S2, ESP32-S3, ESP32-C3, Pico W, etc.)
- A BME280 sensor wired over I2C (SCL/SDA)
- A free Tendrl account
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:
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:
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:
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:
{
"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
- Add validation: attach a service with range rules on
temperature/humidity/pressureso a faulty sensor reading gets flagged instead of silently stored - Wire up an alert: a Strand workflow triggered on the
weathertag can post to Slack or email when a reading crosses a threshold, exactly like the ESP32 + MicroPython guide - Track more state: push battery level, uptime, or sensor calibration data to the state table so it's visible without waiting for the next publish
Tendrl