Docs / Contact / sdks/micropython/api-reference

MicroPython Client API Reference

Complete method reference for the tendrl.Client class on MicroPython.

Lifecycle

Client(**kwargs)

Create a new client instance. See Configuration for all parameters.

python

from tendrl import Client
client = Client(debug=True)

client.start()

Start the client. Connects to WiFi/Ethernet, syncs NTP, fetches entity info, connects to MQTT, and begins background processing.

client.stop()

Stop the client gracefully. Disconnects MQTT and stops timers.

await client.async_stop()

Async version of stop (for async mode).

Publishing

client.publish(data, tags=None, entity="", write_offline=False, db_ttl=0)

Publish a message to Contact via MQTT.

Parameter Type Default Description
data dict/str Message payload
tags list[str] None Tags for flow routing (max 10)
entity str "" Target entity (empty = self)
write_offline bool False Also store locally for offline resilience (managed mode only)
db_ttl int 0 Offline storage TTL in seconds (0 = no expiry)

@client.tether(write_offline=False, db_ttl=None, tags=None, entity="")

Decorator that publishes the return value of a function.

python

@client.tether(tags=["sensor"])
def read_temp():
    return {"temp": 25.5}

read_temp()  # Auto-publishes

Works with both sync and async functions.

State Table

The state table is a persistent key-value store for the entity, sent over MQTT. These methods publish immediately (they are not queued or stored offline).

client.update_state(data, tags=None)

Merge data into the entity's existing state table. Only the supplied keys change.

python

client.update_state({"mode": "standby"})

client.replace_state(data, tags=None)

Replace the entity's entire state table with data.

python

client.replace_state({"firmware": "1.2.0", "mode": "active"})
Info

There is no on-device method to read the state table back. To read the current state, fetch it over REST with GET /api/entities/status-table (see the REST Protocol Guide), or subscribe to remote state changes with a state_callback (below).

Message Receiving

Messages are received automatically via MQTT subscription. Route inbound messages with @client.on(), the inbound counterpart to @client.tether():

python

client = Client(debug=True)

@client.on(tag="ai-response")
def handle_ai_reply(message):
    print(message.get("data"))

@client.on(tags=["alert", "anomaly"])
def handle_alert(message):
    print(message.get("data"))

client.start()

@client.on(msg_type=None, tag=None, tags=None, tags_all=None)

Register a handler for incoming messages. All specified criteria must match (AND semantics). Routes are checked in registration order; first match wins.

Parameter Type Description
msg_type str Match this message type (e.g. "publish")
tag str Match a single tag on the message
tags list[str] Match if the message has any of these tags
tags_all list[str] Match if the message has all of these tags
python

@client.on(msg_type="publish", tag="config-update")
def apply_config(message):
    apply_settings(message.get("data", {}))

@client.on(tags_all=["validated-fail", "sensor"])
def handle_validation_error(message):
    client.update_state({"status": "needs_maintenance"})

@client.on_default

Catch-all handler for messages that don't match any @client.on() route:

python

@client.on_default
def unhandled(message):
    print("No route:", message.get("msg_type"), message.get("tags"))

Fallback callback=

Pass callback= to the constructor for a catch-all handler. It runs when no @client.on() route or @client.on_default handler matches:

python

client = Client(callback=my_handler)

The handler receives a dict with message data, source, timestamp, and tags.

@client.on_state

Subscribe to remote state table updates over MQTT (MicroPython) or via polling at check_msg_rate (Python, Go, JavaScript). Register before client.start():

python

@client.on_state
def handle_state(state):
    if state.get("status") == "needs_maintenance":
        client.update_state({"local_status": "checking"})

Fallback state_callback=

Constructor argument. Runs when no @client.on_state handler is registered:

python

client = Client(state_callback=my_state_handler)

Called when the entity's state table is updated remotely.

Streaming

client.start_streaming(capture_frame_func=None, target_fps=15, quality=70, framesize="QVGA", stream_duration=-1)

Start JPEG video streaming. Requires async mode and the optional streaming module.

Parameter Type Default Description
capture_frame_func callable None Function that returns a JPEG frame. If None, the built-in OpenMV camera capture is used (requires the sensor module).
target_fps int 15 Target frames per second
quality int 70 JPEG compression quality
framesize str "QVGA" Resolution: QQVGA, QVGA, VGA
stream_duration int -1 Stream length in seconds (-1 = run until stopped)

File Transfer

Send and receive files over HTTP. Files are malware-scanned by Surface before delivery and deleted once downloaded. See File Transfer.

client.send_file(path=None, data=None, filename=None, dest="", tags=None, kind="")

Upload a file (from path or data bytes) to an entity (dest) or tag-routed automation (tags). kind="clip" marks a captured motion clip that persists for review. Returns the response dict or None.

client.check_files(limit=50) / client.download_file(transfer_id)

List clean files addressed to this entity, and download one's bytes.

client.send_clip_burst(capture_frame, dest="", tags=None, duration_s=10, fps=10, filename="clip.zip", scale=None, quality=85, meta=None)

Capture JPEG frames into a zip and upload as kind="clip" with meta.format=jpeg_burst. Contact transcodes the burst into an MP4 with browser playback controls. capture_frame is a zero-arg callable returning an OpenMV image; cam.snapshot from Vision satisfies this directly.

python

from tendrl.vision import Camera, Watch, motion

cam = Camera()

def on_change(state, prev, img):
    if state == "motion":
        client.send_clip_burst(cam.snapshot, duration_s=5, fps=8)

watch = Watch(client, detect=motion(), debounce=2, cooldown_s=30,
              on_change=on_change)

while True:
    watch.update(cam.snapshot())

client.send_clip(capture_frame, ...) (deprecated)

Legacy animated GIF capture (OpenMV gif module). Prefer send_clip_burst() for better quality and video controls.

Vision

Optional extension (package-vision.json), OpenMV only. Full reference: Vision.

Camera(mode="detect", framesize="QVGA", quality=90, window=None, skip_ms=2000)

Owns sensor setup and hides the OpenMV csi (current) vs sensor (legacy) split.

Re-initialising a camera is a one-shot operation on some boards

Building a second Camera, re-applying a window, or reconfiguring while a model is loaded can hang an OpenMV AE3 outright: REPL dead, USB dropped, and a soft reset won't bring it back. All three were measured on real hardware.

Safe pattern: free your detector first (drop the reference and gc.collect()), then call reconfigure(). If you need a different window, don't reconfigure; reset the board and build a fresh Camera, because a window is only safe immediately after the reset inside Camera().

reconfigure() clears self.window for that reason, so don't assume the frame shape; read it back with img.width() / img.height(). On the AE3's sensor those differ from the nominal framesize anyway (QVGA returns 320×200, not 320×240).

Detectors

Each returns detect(img) -> (state, confidence) and owns its own state labels. A detector may return (None, 0.0) for "no verdict this frame".

Detector Notes
roi_brightness(roi, threshold, states) Region brightness. No model, no heap. The workhorse.
color_blob(thresholds, roi, min_pixels, states) Lamps, LEDs, signal towers.
motion(threshold=0.02, scale=0.25, states) Frame differencing. threshold is a 0.0–1.0 fraction (RT1062: ~0.02 = person moving, ~0.10+ = near-total change).
person(min_conf=0.4) OpenMV's in-flash model: weights in flash, ~275 KB arena to load, 0 per inference.
face(min_conf=0.4) In-flash MediaPipe BlazeFace. ~390 KB arena, ~14 ms/frame (AE3). The cheapest model detector.
palm(min_conf=0.4) In-flash MediaPipe BlazePalm. ~870 KB arena, ~44 ms/frame (AE3). Wants an NPU.
hands(min_conf=0.4, palm_conf=0.4) Hand presence confirmed by the 21-joint landmark model (two models, ~1.8 MB). Wants an NPU.
model_detect(path, min_conf, postprocess, states) Any TFLite/FOMO/YOLO model — a /rom model or a file on /flash, /sd, /app. Loads via load_model; flattens a classifier's (1, N) output.

The in-flash models come from OpenMV firmware, not the SDK, so asking for one this firmware doesn't ship raises VisionError naming the file. The MediaPipe detectors want square input: Camera(framesize="VGA", window=(400, 400)).

Boxes and keypoints

Geometry instead of a state, for drawing an overlay. All coordinates are image pixels.

Function Returns
person_boxes(min_conf=0.4) [(x, y, w, h, score), ...]
face_boxes(min_conf=0.4) + keypoints as (6, 2): eyes, nose, mouth, ears
palm_boxes(min_conf=0.4) + keypoints as (7, 2): wrist, knuckles, thumb
hand_landmarks(min_conf=0.4, palm_conf=0.4) + keypoints, label as (21, 3) joints, "left"/"right"

All three draw in place, so the overlay survives JPEG compression into a stream or clip.

roi_level(img, roi=None)

Mean brightness of a region as a 0.0–1.0 fraction, normalised across pixel formats. Use it to pick a threshold: print it in a loop while the thing you care about changes.

Watch(client, detect, debounce=3, cooldown_s=0, tags=None, on_change=None, entity="", publish=True, emit_initial=True, debug=False)

Debounces a detector and publishes only on transition: {"state": "empty", "prev": "full", "conf": 0.81}. A stable scene publishes nothing.

Local Database

client.db_put(data, ttl=0, tags=None)

Store data in the local BTree database. Returns the generated key. ttl is in seconds (0 = no expiry).

There is no key parameter: keys are generated, so the client helpers cannot write a stable, overwritable row. For a fixed key (config, thresholds, anything you want to get back by name) create a standalone MicroTetherDB handle and use its key= argument.

client.db_get(key)

Retrieve data by key.

client.db_query(filter)

Query stored data. Returns a list of matching entries.

client.db_delete(key)

Delete an entry by key.

Utilities

tendrl.iso8601(timestamp=None)

Format a timestamp as ISO 8601 string. Uses current time if no argument.

tendrl.free(bytes_only=False)

Get memory and disk statistics:

python

{
    "mem_free": 123456,
    "mem_total": 520000,
    "disk_free": 2000000,
    "disk_size": 4000000
}

tendrl.sample_metrics(refresh_disk=True)

Sample the board's heap, filesystem and PSRAM in the shape the Device tab's live metrics rail consumes:

python

{"h": 132736, "a": 33856, "df": 1949696, "dt": 1966080, "p": None}
# h  free heap        a   allocated heap
# df disk free        dt  disk total        p  free PSRAM (None if absent)

Picks the right filesystem root automatically (/flash on OpenMV, / elsewhere). Pass refresh_disk=False to skip the os.statvfs call and reuse the cached disk figures, which is worth doing in a loop, since heap is what moves.

tendrl.emit_metrics(refresh_disk=True)

Print a TDLMETRIC: line that the Device tab routes to the live metrics rail, hidden from the terminal scrollback. Call it from your own loop to keep the rail updating while your script owns the REPL:

python

from tendrl import emit_metrics

while True:
    # ...your work...
    emit_metrics(refresh_disk=False)

This is the metrics counterpart to preview.frame(). See Live metrics for how to read the result.

MQTT Details

The client uses MQTT QoS 1 (at least once delivery) for all messages. Connection is managed automatically with reconnection handled by the umqtt.robust library.

Topic Structure

Topic Direction Purpose
{account}/{region}/{apiKeyId}/publish Outbound Publish data
{account}/{region}/{apiKeyId}/messages Inbound Receive messages
{account}/{region}/{apiKeyId}/state Inbound Receive retained state updates

Topics are constructed automatically from the entity info returned by the API.

Authentication fields used by the broker: