Skip to main content

plexus-python

Thin Python SDK for Plexus. Send telemetry to the Plexus gateway in one line. Storage, dashboards, alerts, and fleet management live in the platform — this package just ships your data.

PyPI License

Quick Start

pip install plexus-python
from plexus import Plexus

px = Plexus(api_key="plx_xxx", source_id="device-001")
px.send("temperature", 72.5)

Get an API key at app.plexus.company → Devices → Add Device.

Device identity

Every device needs a unique source_id. The recommended way to set one on a real host is the bootstrap script, which requires a device name up front:

curl -sL https://app.plexus.company/setup | bash -s -- \
  --key plx_xxx --name drone-01

The name must match ^[a-z0-9][a-z0-9._-]*$ (max 256 chars). setup.sh refuses to run without --name (or without a TTY to prompt for one) — this is deliberate, because the previous hostname fallback silently merged telemetry from cloned SD-card images that all booted as raspberrypi.

Names are not auto-deduplicated. The gateway echoes back whatever source_id you declare, unchanged — pick a unique name per device (that's what --name and source_id=... are for). Two devices that declare the same name write into the same source.

In normal code, you usually just pass source_id=... explicitly to Plexus(...) and never have to think about it.

Core methods

send(metric, value) — stream a reading

The main method. Call it every time you have a new sensor reading.

px = Plexus(source_id="rig-01")   # reads PLEXUS_API_KEY from env

px.send("engine.rpm", 3450)
px.send("coolant.temp", 82.3)

metric is a dot-namespaced string ("motor.rpm", "gps.fix_quality"). value accepts any JSON-serializable type:

Type Example When to use
float / int 72.5, 3450 Sensor readings, counters
str "RUNNING", "E_STALL" State machines, error codes
bool True Binary flags
dict {"x": 1.5, "y": 2.3} Vectors, structured readings
list [0.5, 1.2, -0.3] Waveforms, joint angles

Optional arguments:

  • tags={"motor_id": "A1"} — key-value labels for filtering in the dashboard
  • timestamp=t — explicit Unix timestamp in seconds; omit to let the SDK pick (see Timestamps)

send_batch(points) — send multiple readings at once

Use this when you sample several sensors together and want them to share a timestamp and land in one network call.

px.send_batch([
    ("temperature", 22.4),
    ("humidity",    58.1),
    ("pressure",    1013.2),
])

points is a list of (metric, value) tuples, or (metric, value, timestamp) 3-tuples when you need a per-point timestamp. Points without their own timestamp share the batch timestamp (now, unless you pass timestamp=t).

batch() — coalesce a fast stream of readings

Use this above a few readings per second. Every send() is one WebSocket message, and the gateway limits messages, not points — 500/s on a connection. Eight channels at 100 Hz sent one at a time is 800 messages/s, and the overflow is discarded before it is stored.

with px.batch(interval_ms=50) as b:
    while running:
        b.send("att.pos_x", att.x)
        b.send("att.rate_x", gyro.x)
        b.send("frames.captured", grabber.count)

b.send() takes the same arguments as px.send(). A background thread flushes the queue every interval_ms, and leaving the block flushes what is left, so nothing is stranded. Readings keep the timestamp they were taken at, not the one they were flushed at.

If the gateway does discard frames it reports RATE_LIMITED; the SDK counts those on px.rate_limited_frames and raises RateLimitedError on the next send rather than letting the loss pass unnoticed.

run(name) — mark a test run

A run is a named window on a source. Runs are recalled on /runs, compared against each other aligned at T+0, and checked against their pass criteria when they close.

with px.run("hotfire-03", pass_criteria=[
    {"metric": "motor.temp_c", "operator": "<", "value": 85},
]) as run:
    bench.execute()

Leaving the block closes the run as completed; an exception closes it as aborted and re-raises. Use px.start_run() / px.end_run() when the two halves happen in different places — end_run() returns the run with its verdict in test_result.

event(name, data) — record a discrete occurrence

Use event() for things that happen rather than things you measure continuously. Faults, state transitions, operator actions, log entries — anything you'd put on a timeline as a marker rather than plot as a graph.

px.event("fault",        "E-stop triggered")
px.event("state_change", {"from": "IDLE", "to": "RUNNING"})
px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})

The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.

Video streaming

Two methods depending on whether you control the capture loop or just have a URL.

send_video_frame(frame, camera_id) — send frames you capture yourself

Use this when your code owns the capture loop — a picamera2 callback, an OpenCV VideoCapture loop, or an FFmpeg pipe you manage. Pass each frame and the SDK ships it to Plexus over WebSocket.

import cv2

cap = cv2.VideoCapture(0)
while True:
    ok, frame = cap.read()
    if ok:
        px.send_video_frame(frame, camera_id="front")

Accepted frame types:

  • numpy ndarray (H × W × C) — from OpenCV or picamera2; requires opencv-python
  • JPEG bytes — passed through as-is, zero re-encode overhead
  • Other image bytes (PNG, BMP, WebP) — decoded and re-encoded as JPEG via Pillow; requires pip install plexus-python[video]

camera_id identifies which camera the frame came from. Use distinct IDs when streaming from multiple cameras simultaneously ("front", "rear", "cam:0").

stream_camera(url, camera_id) — stream from an RTSP URL or file

Use this when you have an RTSP stream or video file and don't want to manage the capture loop yourself. The SDK runs FFmpeg internally and handles the rest. Requires FFmpeg on $PATH.

stop = px.stream_camera("rtsp://192.168.1.100/stream", camera_id="front")
# ... do other work ...
stop.set()  # stop streaming

Returns a threading.Event — call .set() to stop. Runs in a background thread so it doesn't block your main loop.

Which to use: if you're piping from rpicam-vid, picamera2, or your own capture process, use send_video_frame(). If you have an RTSP URL or file path, use stream_camera().

Bring Your Own Protocol

This package ships no adapters, auto-detection, or daemons — just the client. Use whatever library you'd use anyway and pipe values into px.send().

# MAVLink (pymavlink)
for msg in conn:
    if msg.get_type() == "ATTITUDE":
        px.send("attitude.roll", msg.roll)

# CAN (python-can)
for msg in bus:
    px.send(f"can.0x{msg.arbitration_id:x}", int.from_bytes(msg.data, "big"))

# MQTT (paho-mqtt)
def on_message(_c, _u, msg):
    px.send(msg.topic.replace("/", "."), float(msg.payload))

# I2C sensor (Adafruit CircuitPython)
px.send("temperature", bme.temperature)

See examples/ for runnable versions of each.

Reliability

Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. Enable SQLite persistence to survive restarts and power loss:

px = Plexus(persistent_buffer=True)

Point counts and flush:

px.buffer_size()
px.flush_buffer()

Timestamps and clock correction

By default — px.send("temp", 72.5) with no timestamp argument — the SDK picks the time itself. Over WebSocket, it synchronizes with the gateway clock on every connection, so data lands at the right place on the timeline even if the device's system clock is wrong (no NTP on first boot, stale RTC, fresh OS image).

px.send("temperature", 72.5)                # SDK picks time; gateway-synced over WS
px.send("temperature", 72.5, timestamp=t)   # your timestamp, used as-is, no correction

Pass an explicit timestamp when you have a reliable external time source (GPS, trusted RTC, host NTP) or are replaying historical data with known timestamps.

Omit timestamp when the device may have booted without NTP — which is the default on Raspberry Pi, Jetson, and most embedded Linux boards without a network connection at first boot.

Known limits:

  • Clock sync refreshes on WebSocket (re)connect. A device with a drifting RTC that stays connected for many days accumulates uncorrected drift between reconnects.
  • The HTTP fallback path (used when the WebSocket is unavailable) does not receive clock sync — timestamps default to the uncorrected device clock.
  • send_batch() shares one timestamp across the batch by default; pass (metric, value, timestamp) 3-tuples for per-point timestamps.

Transport

By default the SDK connects over a WebSocket to /ws/device on the gateway — the gateway's device wire protocol. This gives you:

  • lower-latency streaming of telemetry,
  • the channel that will carry actions triggered from a Plexus dashboard.

If the socket is unavailable, sends transparently fall back to POST /ingest so no data is lost.

# ws with transparent http fallback — this is the only mode
px = Plexus()

There is no transport selector: the SDK always prefers the WebSocket and falls back to POST /ingest on its own when the socket is unavailable.

Handling commands

Register a handler before the first send() so the command is advertised in the auth frame:

def reboot(name, params):
    delay = params.get("delay_s", 0)
    # ... reboot logic ...
    return {"ok": True, "delay": delay}

px = Plexus()
px.on_command("reboot", reboot, description="reboot the device")
px.send("temperature", 72.5)   # opens the socket, waits for auth

The SDK sends an ack frame before invoking the handler, then a result frame with whatever the handler returns (or an error frame if it raises).

Note: nothing in Plexus can currently trigger a custom handler. The API route for sending commands was turned off on 2026-09-21. Triggering handlers from a dashboard, with permissions and a record of every run, is being rebuilt.

Environment Variables

Variable Description Default
PLEXUS_API_KEY API key (required) none
PLEXUS_GATEWAY_URL HTTP ingest URL https://gateway.plexus.company
PLEXUS_GATEWAY_WS_URL WebSocket URL wss://gateway.plexus.company

Agent skills

Three skills ship with the package and teach a coding agent the Plexus API — the endpoints, the live stream, and the mistakes that produce a silent 400.

plexus skills install          # -> ~/.claude/skills
plexus skills install --project  # -> ./.claude/skills, travels with the repo

Then ask for what you want in plain language: "send my ESP32's battery voltage to Plexus", "build me a fleet dashboard". Plain Markdown, no install, no credentials. See skills/README.md.

Architecture

Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard

One thin path. No agent, no daemon, no adapters. If you want the full HardwareOps platform — dashboards, alerts, RCA, fleet views — that's the web UI at app.plexus.company. This package gets your data there.

License

Apache 2.0

Release files for plexus-python 0.11.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for plexus-python 0.11.5
File Size Uploaded
plexus_python-0.11.5.tar.gz 214.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for plexus-python 0.11.5
File Interpreter ABI Platform
plexus_python-0.11.5-py3-none-any.whl Python 3 none any Details

Total release size: 287.0 kB

Release files / plexus_python-0.11.5.tar.gz

Download URL plexus_python-0.11.5.tar.gz
Size 214.9 kB
Tags Source
SHA-256 checksum
How to use checksums
72a0627734adc68d7a0559ce71d427bdb01a06551c3f3bd3d9b9269cde0e4996
BLAKE2b-256 checksum
How to use checksums
eca748ae5948c33eceb717d65badcf2f292b453dc1bc2cc64c86cd68f3a636c1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / plexus_python-0.11.5-py3-none-any.whl

Download URL plexus_python-0.11.5-py3-none-any.whl
Size 72.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6cd0488634914fa37099cc11eabf3d6dd696fedb4f1e56b43d301516aec1f642
BLAKE2b-256 checksum
How to use checksums
e3916fc4f5d6043d5fcef53f46ac6169623e426ac7a3494d1f620bdc247447df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

0.12.0

2 release files

0.11.6

2 release files

This release

0.11.5 This release

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.4.9

2 release files

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page