plexus-python 0.11.4__tar.gz → 0.11.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/ISSUE_TEMPLATE/bug_report.yml +4 -4
- {plexus_python-0.11.4 → plexus_python-0.11.6}/AGENTS.md +14 -5
- {plexus_python-0.11.4 → plexus_python-0.11.6}/API.md +61 -14
- {plexus_python-0.11.4 → plexus_python-0.11.6}/CHANGELOG.md +85 -1
- {plexus_python-0.11.4 → plexus_python-0.11.6}/PKG-INFO +30 -8
- {plexus_python-0.11.4 → plexus_python-0.11.6}/README.md +29 -7
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/__init__.py +1 -1
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/batching.py +4 -5
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/client.py +9 -9
- {plexus_python-0.11.4 → plexus_python-0.11.6}/pyproject.toml +1 -1
- plexus_python-0.11.6/scripts/setup.sh +462 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/scripts/verify_skills.py +3 -2
- {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/README.md +3 -2
- {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/plexus/SKILL.md +21 -17
- {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/plexus-dashboard/SKILL.md +6 -6
- {plexus_python-0.11.4 → plexus_python-0.11.6}/skills/plexus-firmware/SKILL.md +19 -12
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_skills.py +12 -2
- {plexus_python-0.11.4 → plexus_python-0.11.6}/uv.lock +1 -1
- plexus_python-0.11.4/scripts/setup.sh +0 -351
- {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/.gitignore +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/CONTRIBUTING.md +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/LICENSE +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/SECURITY.md +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/.python-version +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/README.md +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/basic.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/can.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/mac_metrics.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/mavlink.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/mqtt.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/pyproject.toml +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/thermal_camera.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/examples/uv.lock +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/_log.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/buffer.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/cameras/__init__.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/cameras/thermal.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/cli.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/config.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/plexus/ws.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/scripts/release.sh +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/conftest.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_basic.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_batching.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_batching_wire.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_buffer.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_cli_diagnostics.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_config.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_isolation.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_rate_limit.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_retry.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_runs.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_thermal.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_video.py +0 -0
- {plexus_python-0.11.4 → plexus_python-0.11.6}/tests/test_ws.py +0 -0
|
@@ -6,9 +6,9 @@ body:
|
|
|
6
6
|
- type: input
|
|
7
7
|
id: agent-version
|
|
8
8
|
attributes:
|
|
9
|
-
label:
|
|
9
|
+
label: SDK Version
|
|
10
10
|
description: Output of `plexus --version` or version in pyproject.toml
|
|
11
|
-
placeholder: "0.
|
|
11
|
+
placeholder: "0.11.5"
|
|
12
12
|
validations:
|
|
13
13
|
required: true
|
|
14
14
|
|
|
@@ -57,8 +57,8 @@ body:
|
|
|
57
57
|
id: extras
|
|
58
58
|
attributes:
|
|
59
59
|
label: Installed Extras
|
|
60
|
-
description: Which optional extras are installed?
|
|
61
|
-
placeholder: "pip install plexus-python[
|
|
60
|
+
description: Which optional extras are installed? The only one is `video`.
|
|
61
|
+
placeholder: "pip install plexus-python[video]"
|
|
62
62
|
|
|
63
63
|
- type: textarea
|
|
64
64
|
id: logs
|
|
@@ -24,7 +24,8 @@ plexus whoami # Show the local credential summary
|
|
|
24
24
|
|
|
25
25
|
`plexus init` opens a browser to `app.plexus.company/auth/cli`, waits for the callback, and
|
|
26
26
|
persists the issued key to `~/.plexus/config.json`. Alternatively, set `PLEXUS_API_KEY` (or pass
|
|
27
|
-
`api_key=` to `Plexus()`) instead of running `init`; get a key at app.plexus.company/api.
|
|
27
|
+
`api_key=` to `Plexus()`) instead of running `init`; get a key at app.plexus.company/api. The optional
|
|
28
|
+
"Limit to device slug" field there binds a key to one `source_id`; use it for devices in customer hands.
|
|
28
29
|
|
|
29
30
|
## Exit Codes
|
|
30
31
|
|
|
@@ -43,14 +44,22 @@ px = Plexus(api_key="plx_xxxxx", source_id="device-001")
|
|
|
43
44
|
px.send("temperature", 72.5)
|
|
44
45
|
px.send("pressure", 1013.25, tags={"unit": "hPa"})
|
|
45
46
|
|
|
46
|
-
#
|
|
47
|
+
# One message for a list of readings
|
|
47
48
|
px.send_batch([
|
|
48
49
|
("temperature", 72.5),
|
|
49
50
|
("pressure", 1013.25),
|
|
50
51
|
])
|
|
51
52
|
|
|
52
|
-
#
|
|
53
|
-
|
|
53
|
+
# send() is one message per call and does not batch. Above a few readings
|
|
54
|
+
# a second, let a background thread group them:
|
|
55
|
+
with px.batch(interval_ms=50) as b:
|
|
56
|
+
b.send("temperature", 72.5)
|
|
57
|
+
|
|
58
|
+
# Events (faults, state changes, log lines). No log upload, no logging.Handler.
|
|
59
|
+
px.event("log", {"level": "error", "msg": "IMU read timed out"})
|
|
60
|
+
|
|
61
|
+
# The on-disk (SQLite) buffer is on by default; this keeps it in memory only
|
|
62
|
+
px = Plexus(api_key="plx_xxxxx", persistent_buffer=False)
|
|
54
63
|
```
|
|
55
64
|
|
|
56
65
|
## Key Conventions
|
|
@@ -58,5 +67,5 @@ px = Plexus(api_key="plx_xxxxx", persistent_buffer=True)
|
|
|
58
67
|
- Config lives in `~/.plexus/config.json`
|
|
59
68
|
- API keys are prefixed with `plx_`
|
|
60
69
|
- Source IDs (device slugs) namespace metrics
|
|
61
|
-
- HTTP ingest → `POST /ingest` on gateway; WebSocket → `/ws/device` for streaming
|
|
70
|
+
- HTTP ingest → `POST /ingest` on gateway; WebSocket → `/ws/device` for streaming (paid plans; the SDK falls back to HTTP on Free)
|
|
62
71
|
- Gateway resolves `org_id` server-side from the API key — clients do not supply it
|
|
@@ -9,7 +9,13 @@ Send telemetry data to Plexus using HTTP or WebSocket.
|
|
|
9
9
|
| Method | Use Case |
|
|
10
10
|
| --------- | ----------------------------------------------- |
|
|
11
11
|
| HTTP POST | Simple scripts, batch uploads, embedded devices |
|
|
12
|
-
| WebSocket | Real-time streaming
|
|
12
|
+
| WebSocket | Real-time streaming and video (paid plans) |
|
|
13
|
+
|
|
14
|
+
The Python SDK picks for you: it tries the WebSocket and falls back to HTTP on
|
|
15
|
+
its own. On the Free plan the gateway refuses the device WebSocket
|
|
16
|
+
(`streaming_requires_plan`), so the SDK sends everything over HTTP and
|
|
17
|
+
telemetry and events still land. Live streaming and video need a paid plan.
|
|
18
|
+
Free also allows up to 3 devices and 7 days of history.
|
|
13
19
|
|
|
14
20
|
## Quick Start
|
|
15
21
|
|
|
@@ -18,12 +24,12 @@ Send telemetry data to Plexus using HTTP or WebSocket.
|
|
|
18
24
|
Set up your device with one command using an API key:
|
|
19
25
|
|
|
20
26
|
```bash
|
|
21
|
-
# With API key (
|
|
27
|
+
# With an API key (get one at app.plexus.company/api)
|
|
22
28
|
curl -sL https://app.plexus.company/setup | bash -s -- --key plx_your_api_key
|
|
23
29
|
|
|
24
30
|
```
|
|
25
31
|
|
|
26
|
-
Then
|
|
32
|
+
Then find the device at [app.plexus.company/devices](https://app.plexus.company/devices).
|
|
27
33
|
|
|
28
34
|
### Option 2: Direct HTTP
|
|
29
35
|
|
|
@@ -63,9 +69,16 @@ Plexus uses API keys for all authentication:
|
|
|
63
69
|
**Option B: Manual creation**
|
|
64
70
|
|
|
65
71
|
1. Sign up at [app.plexus.company](https://app.plexus.company)
|
|
66
|
-
2. Go to
|
|
72
|
+
2. Go to [app.plexus.company/api](https://app.plexus.company/api)
|
|
67
73
|
3. Create an API key (starts with `plx_`)
|
|
68
74
|
|
|
75
|
+
**Limit to device slug** (optional, on the same form) binds the key to one
|
|
76
|
+
`source_id`. The gateway then refuses it for any other source: `403` on
|
|
77
|
+
`/ingest`, and a `source_not_allowed` error on the WebSocket. Use a bound key
|
|
78
|
+
on every device you put in a customer's hands, so a key pulled off one unit
|
|
79
|
+
cannot write as the rest of your fleet. Leave it empty for a fleet key that can
|
|
80
|
+
write as any source in your org.
|
|
81
|
+
|
|
69
82
|
## HTTP API
|
|
70
83
|
|
|
71
84
|
### Authentication
|
|
@@ -97,6 +110,7 @@ x-api-key: plx_xxxxx
|
|
|
97
110
|
|
|
98
111
|
| Field | Type | Required | Description |
|
|
99
112
|
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `class` | string | WS only | `"metric"` (numeric value) or `"event"` (anything else). Set it explicitly. Over HTTP a missing `class` is inferred from the value; over WebSocket it is required. |
|
|
100
114
|
| `metric` | string | Yes | Metric name (e.g., `temperature`, `motor.rpm`) |
|
|
101
115
|
| `value` | any | Yes | See supported value types below |
|
|
102
116
|
| `timestamp` | float | No | Unix timestamp in seconds (or ms if ≥ 1e12). Omit to use device time. Over WebSocket, the Python SDK applies a server-synced clock correction when omitted — see [Clock correction](#clock-correction). |
|
|
@@ -113,6 +127,27 @@ x-api-key: plx_xxxxx
|
|
|
113
127
|
| object | `{"x": 1.2, "y": 3.4, "z": 5.6}` | Vector data, structured readings |
|
|
114
128
|
| array | `[1.0, 2.0, 3.0, 4.0]` | Waveforms, multiple values |
|
|
115
129
|
|
|
130
|
+
### Events and logs
|
|
131
|
+
|
|
132
|
+
Things that happen, rather than things you measure, are `class: "event"`
|
|
133
|
+
points: faults, state changes, operator actions, log lines. The Python SDK
|
|
134
|
+
sends them with `px.event(name, data)`.
|
|
135
|
+
|
|
136
|
+
Limits per event point: a string value up to 256 bytes, an object or array
|
|
137
|
+
value up to 4,096 bytes of JSON, and up to 16 tags. The gateway rejects larger
|
|
138
|
+
points with a 400.
|
|
139
|
+
|
|
140
|
+
There is no log-file upload and no `logging.Handler`. Forward the log lines
|
|
141
|
+
that matter as events:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
px.event("log", {"level": "error", "msg": "IMU read timed out"})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Send errors, warnings and state changes, not every debug line: each call is
|
|
148
|
+
one message, and the gateway limits messages (see
|
|
149
|
+
[Rate limits and batching](#rate-limits-and-batching)).
|
|
150
|
+
|
|
116
151
|
### Runs
|
|
117
152
|
|
|
118
153
|
A **run** is a named time window on a source — a hot-fire, a bench sequence, a
|
|
@@ -158,14 +193,21 @@ To group data without a window, plain `tags` on each point still work.
|
|
|
158
193
|
|
|
159
194
|
## WebSocket API
|
|
160
195
|
|
|
161
|
-
For real-time
|
|
196
|
+
For real-time streaming and video, devices connect via WebSocket. This needs a
|
|
197
|
+
paid plan: on Free, the gateway answers `device_auth` with an error frame
|
|
198
|
+
(`"code": "streaming_requires_plan"`) and closes. Send over HTTP instead; the
|
|
199
|
+
Python SDK does this for you.
|
|
200
|
+
|
|
201
|
+
Video frames go over this socket. They are relayed live to anyone watching and
|
|
202
|
+
stored only when someone presses **Record** in the app, for up to 4 hours per
|
|
203
|
+
recording.
|
|
162
204
|
|
|
163
205
|
### Connection Flow
|
|
164
206
|
|
|
165
207
|
1. Device connects to the gateway
|
|
166
208
|
2. Device authenticates with API key (and advertises any registered commands)
|
|
167
209
|
3. Device streams `telemetry` frames
|
|
168
|
-
4.
|
|
210
|
+
4. Plexus invokes registered commands via `typed_command` (see the note under Commands)
|
|
169
211
|
|
|
170
212
|
### Device Authentication
|
|
171
213
|
|
|
@@ -227,7 +269,9 @@ Devices authenticate using an API key. The gateway echoes the declared `source_i
|
|
|
227
269
|
|
|
228
270
|
### Commands
|
|
229
271
|
|
|
230
|
-
|
|
272
|
+
Commands reach the device as a single `typed_command` envelope; the device replies with `command_result` frames. Register handlers with `px.on_command(...)` before the first `send()`.
|
|
273
|
+
|
|
274
|
+
> **Note:** the public API route for sending commands (`POST /v1/sources/{id}/commands`) was turned off on 2026-09-21 and returns `410 Gone`. Nothing in Plexus can currently trigger a custom handler.
|
|
231
275
|
|
|
232
276
|
```json
|
|
233
277
|
// Server → Device
|
|
@@ -275,6 +319,7 @@ await fetch("https://gateway.plexus.company/ingest", {
|
|
|
275
319
|
body: JSON.stringify({
|
|
276
320
|
points: [
|
|
277
321
|
{
|
|
322
|
+
class: "metric",
|
|
278
323
|
metric: "temperature",
|
|
279
324
|
value: 72.5,
|
|
280
325
|
timestamp: Date.now() / 1000,
|
|
@@ -333,6 +378,7 @@ void sendToPlexus(const char* metric, float value) {
|
|
|
333
378
|
http.addHeader("x-api-key", "plx_xxxxx");
|
|
334
379
|
|
|
335
380
|
String payload = "{\"points\":[{";
|
|
381
|
+
payload += "\"class\":\"metric\",";
|
|
336
382
|
payload += "\"metric\":\"" + String(metric) + "\",";
|
|
337
383
|
payload += "\"value\":" + String(value) + ",";
|
|
338
384
|
payload += "\"timestamp\":" + String(time(nullptr)) + ",";
|
|
@@ -356,6 +402,7 @@ curl -X POST https://gateway.plexus.company/ingest \
|
|
|
356
402
|
-H "Content-Type: application/json" \
|
|
357
403
|
-d "{
|
|
358
404
|
\"points\": [{
|
|
405
|
+
\"class\": \"metric\",
|
|
359
406
|
\"metric\": \"temperature\",
|
|
360
407
|
\"value\": 72.5,
|
|
361
408
|
\"timestamp\": $(date +%s),
|
|
@@ -406,25 +453,25 @@ while True:
|
|
|
406
453
|
| 200 | Success |
|
|
407
454
|
| 400 | Bad request (check JSON format) |
|
|
408
455
|
| 401 | Invalid or missing API key |
|
|
409
|
-
| 403 | API key lacks permissions
|
|
456
|
+
| 403 | API key lacks permissions, or is limited to another device |
|
|
410
457
|
| 404 | Resource not found |
|
|
411
458
|
| 410 | Resource expired |
|
|
412
459
|
|
|
413
460
|
## Rate limits and batching
|
|
414
461
|
|
|
415
462
|
The gateway meters **messages**, not points. One `px.send()` call is one
|
|
416
|
-
message, whatever it carries:
|
|
463
|
+
message, whatever it carries. `send()` does not batch:
|
|
417
464
|
|
|
418
465
|
| Limit | Value |
|
|
419
466
|
| ---------------------------------- | ---------------- |
|
|
420
|
-
| Telemetry messages per WS connection |
|
|
421
|
-
| Hard ceiling per source (WS + HTTP) |
|
|
467
|
+
| Telemetry messages per WS connection | 2,000/s |
|
|
468
|
+
| Hard ceiling per source (WS + HTTP) | 2,000 messages/s, bursts up to 500 |
|
|
422
469
|
| Points per message | 10,000 |
|
|
423
470
|
| Message size | 1 MB |
|
|
424
471
|
|
|
425
472
|
Because the ceiling counts messages, the shape of your sends decides whether
|
|
426
|
-
you hit it.
|
|
427
|
-
over the limit. The same
|
|
473
|
+
you hit it. 25 channels at 100 Hz sent one at a time is 2,500 messages/s —
|
|
474
|
+
over the limit. The same 2,500 readings/s batched every 100 ms is 10 messages/s,
|
|
428
475
|
and the batches are also several times cheaper to store.
|
|
429
476
|
|
|
430
477
|
**Over the limit, the gateway discards the whole message.** It replies with a
|
|
@@ -493,4 +540,4 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is
|
|
|
493
540
|
- **Omit timestamp when unsure** - The Python SDK applies server-synced clock correction when `timestamp` is omitted over WebSocket; only pass an explicit timestamp when you have a reliable wall-clock source
|
|
494
541
|
- **Consistent source_id** - Use the same ID for each physical device/source
|
|
495
542
|
- **Use tags** - Label data for filtering and grouping (e.g., `{"location": "lab"}`)
|
|
496
|
-
- **
|
|
543
|
+
- **Let the SDK pick the transport** - It uses the WebSocket when your plan allows it and falls back to HTTP otherwise
|
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.11.6] - 2026-09-22 - Docs and skills catch up
|
|
4
|
+
|
|
5
|
+
Docs and agent skills caught up with the platform. The only code change is
|
|
6
|
+
one error message. The skills ship inside the wheel, so this release is how
|
|
7
|
+
they reach users.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **The 0.11.4 entry below overstated the `class` bug.** It said raw-HTTP
|
|
12
|
+
examples without `class` return 400. They don't: `POST /ingest` infers a
|
|
13
|
+
missing `class` (numbers become metrics, everything else events). The
|
|
14
|
+
WebSocket is the path that requires it, and `"class": "metric"` on a
|
|
15
|
+
non-number is still a 400. The skills and `API.md` now say that, and still
|
|
16
|
+
tell raw-HTTP senders to set `class` explicitly. The JavaScript, Arduino
|
|
17
|
+
and Bash examples in `API.md` now set it too.
|
|
18
|
+
|
|
19
|
+
- **Where to get an API key.** The README said "Devices → Add Device" and
|
|
20
|
+
`API.md` said "Settings → Developer". Keys are made at
|
|
21
|
+
`app.plexus.company/api`, and the "no API key" error now says so instead
|
|
22
|
+
of pointing at `/devices`.
|
|
23
|
+
|
|
24
|
+
- **The README said `setup.sh` refuses to run without `--name`.** It
|
|
25
|
+
doesn't. Without a name the SDK makes up a random `source-xxxxxxxx` id.
|
|
26
|
+
The README now says to pass `--name` and what happens if you don't.
|
|
27
|
+
|
|
28
|
+
- **The README said to "enable" the SQLite buffer.** It is on by default.
|
|
29
|
+
The example now shows how to turn it off.
|
|
30
|
+
|
|
31
|
+
- **The firmware skill said `px.send()` is "batched for you".** It is one
|
|
32
|
+
message per call. Only `px.batch()` batches.
|
|
33
|
+
|
|
34
|
+
- **The bug-report template asked which of `sensors`, `can`, `mavlink` and
|
|
35
|
+
`camera` you had installed.** None of those extras exist. The only one is
|
|
36
|
+
`video`.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **The read API host in the skills is `https://api.plexus.company`**, not
|
|
41
|
+
the raw Fly hostname. `scripts/verify_skills.py` checks it by default, and
|
|
42
|
+
a test keeps the old hostname out of the skills.
|
|
43
|
+
|
|
44
|
+
- **Documented "Limit to device slug".** A key made with it can only send as
|
|
45
|
+
that one `source_id`. The docs recommend one for every device in a
|
|
46
|
+
customer's hands.
|
|
47
|
+
|
|
48
|
+
- **Documented the Free plan's limits.** The gateway refuses the device
|
|
49
|
+
WebSocket on Free (`streaming_requires_plan`); the SDK falls back to HTTP,
|
|
50
|
+
so telemetry and events still land. Live streaming and video need a paid
|
|
51
|
+
plan. Free allows 3 devices and 7 days of history.
|
|
52
|
+
|
|
53
|
+
- **Documented how video is kept.** Frames are relayed live, and stored only
|
|
54
|
+
when someone presses Record in the app, up to 4 hours per recording.
|
|
55
|
+
|
|
56
|
+
- **Documented logs.** There is no log upload and no `logging.Handler`.
|
|
57
|
+
Forward the lines that matter with `px.event("log", {...})`. Event limits
|
|
58
|
+
are now written down: 256 bytes for a string value, 4,096 bytes of JSON
|
|
59
|
+
for an object or array, 16 tags.
|
|
60
|
+
|
|
61
|
+
- **`scripts/setup.sh` re-synced with the script `/setup` serves.** The old
|
|
62
|
+
copy still installed `[sensors]` and `[picamera]` extras that were removed
|
|
63
|
+
in 0.2.0. It also links `github.com/plexus-oss/plexus-python` and
|
|
64
|
+
`app.plexus.company/api` now.
|
|
65
|
+
|
|
66
|
+
- **The skills read device events from `/v1/sources/{id}/events`.** It is
|
|
67
|
+
live on the data API as of 2026-09-22. `/logs` still works as an alias.
|
|
68
|
+
|
|
69
|
+
- **Rate limits quote the gateway's current ceiling**: 2,000 messages/s per
|
|
70
|
+
connection and per source, not the old 500/s.
|
|
71
|
+
|
|
72
|
+
## [0.11.5] - 2026-09-21 - The commands API is off
|
|
73
|
+
|
|
74
|
+
No code changes. The docs described a way to send commands to a device that
|
|
75
|
+
no longer exists.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- **`POST /v1/sources/{id}/commands` is turned off, and the docs now say so.**
|
|
80
|
+
Plexus turned the endpoint off on 2026-09-21. It returns `410 Gone`.
|
|
81
|
+
Any API key of any scope could send any command name to any device in
|
|
82
|
+
the org, which is not safe for commands that reach hardware.
|
|
83
|
+
- The agent skill no longer offers to send commands, and it tells agents
|
|
84
|
+
not to call the endpoint.
|
|
85
|
+
- README and `API.md` say plainly that nothing in Plexus can currently
|
|
86
|
+
trigger an `on_command` handler.
|
|
87
|
+
- `on_command` itself is unchanged.
|
|
4
88
|
|
|
5
89
|
## [0.11.4] - 2026-09-08 - Docs that actually work
|
|
6
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: plexus-python
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.6
|
|
4
4
|
Summary: Thin Python SDK for Plexus — send telemetry in one line
|
|
5
5
|
Project-URL: Homepage, https://plexus.company
|
|
6
6
|
Project-URL: Documentation, https://docs.plexus.company
|
|
@@ -55,7 +55,9 @@ px = Plexus(api_key="plx_xxx", source_id="device-001")
|
|
|
55
55
|
px.send("temperature", 72.5)
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
Get an API key at [app.plexus.company](https://app.plexus.company)
|
|
58
|
+
Get an API key at [app.plexus.company/api](https://app.plexus.company/api), or run `plexus init` to authorize the machine in a browser.
|
|
59
|
+
|
|
60
|
+
When you create a key there, you can fill in **Limit to device slug**. That key can then only send as that one `source_id`; the gateway refuses it for any other. Use one of these for every device you put in a customer's hands, so a key pulled off one unit cannot write as the rest of your fleet.
|
|
59
61
|
|
|
60
62
|
## Device identity
|
|
61
63
|
|
|
@@ -66,7 +68,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
|
|
|
66
68
|
--key plx_xxx --name drone-01
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
The name must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). `
|
|
71
|
+
The name is turned into the device's `source_id`, which must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). Pass `--name` every time. Without it, and without `source_id=...` in code, the SDK makes up a random id like `source-1a2b3c4d` on first run and saves it to `~/.plexus/config.json`. Don't use the hostname: cloned SD-card images all boot as `raspberrypi`, and their telemetry merges into one source.
|
|
70
72
|
|
|
71
73
|
**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.
|
|
72
74
|
|
|
@@ -115,7 +117,7 @@ px.send_batch([
|
|
|
115
117
|
|
|
116
118
|
### `batch()` — coalesce a fast stream of readings
|
|
117
119
|
|
|
118
|
-
Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points —
|
|
120
|
+
Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points — 2,000/s on a connection. 25 channels at 100 Hz sent one at a time is 2,500 messages/s, and the overflow is discarded before it is stored.
|
|
119
121
|
|
|
120
122
|
```python
|
|
121
123
|
with px.batch(interval_ms=50) as b:
|
|
@@ -154,10 +156,24 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
|
|
|
154
156
|
|
|
155
157
|
The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
|
|
156
158
|
|
|
159
|
+
Limits per event: a string value up to 256 bytes, a dict or list value up to 4,096 bytes of JSON, and up to 16 tags. The gateway rejects anything larger.
|
|
160
|
+
|
|
161
|
+
### Logs
|
|
162
|
+
|
|
163
|
+
There is no log-file upload and no `logging.Handler` in this package. To get important log lines into Plexus, send them as events:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
px.event("log", {"level": "error", "msg": "IMU read timed out"})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Forward the lines you would want on the timeline next to your telemetry (errors, warnings, state changes), not every debug line. Each call is one message, and the gateway limits messages (see [`batch()`](#batch--coalesce-a-fast-stream-of-readings)).
|
|
170
|
+
|
|
157
171
|
## Video streaming
|
|
158
172
|
|
|
159
173
|
Two methods depending on whether you control the capture loop or just have a URL.
|
|
160
174
|
|
|
175
|
+
Video needs a paid plan: frames go over the WebSocket, which the gateway refuses on the Free plan. Frames are relayed live to anyone watching. They are stored only when someone presses **Record** in the app, for up to 4 hours per recording.
|
|
176
|
+
|
|
161
177
|
### `send_video_frame(frame, camera_id)` — send frames you capture yourself
|
|
162
178
|
|
|
163
179
|
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.
|
|
@@ -219,10 +235,10 @@ See [`examples/`](examples/) for runnable versions of each.
|
|
|
219
235
|
|
|
220
236
|
## Reliability
|
|
221
237
|
|
|
222
|
-
Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages.
|
|
238
|
+
Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. The buffer is on disk (SQLite) by default, so it survives restarts and power loss. To keep it in memory only:
|
|
223
239
|
|
|
224
240
|
```python
|
|
225
|
-
px = Plexus(persistent_buffer=
|
|
241
|
+
px = Plexus(persistent_buffer=False)
|
|
226
242
|
```
|
|
227
243
|
|
|
228
244
|
Point counts and flush:
|
|
@@ -255,7 +271,7 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
|
|
|
255
271
|
By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — the gateway's device wire protocol. This gives you:
|
|
256
272
|
|
|
257
273
|
- lower-latency streaming of telemetry,
|
|
258
|
-
-
|
|
274
|
+
- the channel that will carry actions triggered from a Plexus dashboard.
|
|
259
275
|
|
|
260
276
|
If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
|
|
261
277
|
|
|
@@ -266,6 +282,10 @@ px = Plexus()
|
|
|
266
282
|
|
|
267
283
|
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.
|
|
268
284
|
|
|
285
|
+
Either way, plain `px.send()` is one message per call; it does not batch. `px.send_batch()` sends one list as one message, and `px.batch()` groups a fast stream for you in the background.
|
|
286
|
+
|
|
287
|
+
**On the Free plan** the gateway refuses the device WebSocket (`streaming_requires_plan`). The SDK falls back to HTTP by itself, so `send()`, `send_batch()`, `batch()` and `event()` all still work. Live streaming and video need a paid plan. Free also caps you at 3 devices and 7 days of history.
|
|
288
|
+
|
|
269
289
|
### Handling commands
|
|
270
290
|
|
|
271
291
|
Register a handler before the first `send()` so the command is advertised in the auth frame:
|
|
@@ -283,6 +303,8 @@ px.send("temperature", 72.5) # opens the socket, waits for auth
|
|
|
283
303
|
|
|
284
304
|
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).
|
|
285
305
|
|
|
306
|
+
> **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.
|
|
307
|
+
|
|
286
308
|
## Environment Variables
|
|
287
309
|
|
|
288
310
|
| Variable | Description | Default |
|
|
@@ -308,7 +330,7 @@ credentials. See [skills/README.md](skills/README.md).
|
|
|
308
330
|
## Architecture
|
|
309
331
|
|
|
310
332
|
```
|
|
311
|
-
Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
|
|
333
|
+
Your code ── px.send() ── WebSocket /ws/device (or HTTP POST /ingest) ──> plexus-gateway ──> ClickHouse + Dashboard
|
|
312
334
|
```
|
|
313
335
|
|
|
314
336
|
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.
|
|
@@ -18,7 +18,9 @@ px = Plexus(api_key="plx_xxx", source_id="device-001")
|
|
|
18
18
|
px.send("temperature", 72.5)
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Get an API key at [app.plexus.company](https://app.plexus.company)
|
|
21
|
+
Get an API key at [app.plexus.company/api](https://app.plexus.company/api), or run `plexus init` to authorize the machine in a browser.
|
|
22
|
+
|
|
23
|
+
When you create a key there, you can fill in **Limit to device slug**. That key can then only send as that one `source_id`; the gateway refuses it for any other. Use one of these for every device you put in a customer's hands, so a key pulled off one unit cannot write as the rest of your fleet.
|
|
22
24
|
|
|
23
25
|
## Device identity
|
|
24
26
|
|
|
@@ -29,7 +31,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
|
|
|
29
31
|
--key plx_xxx --name drone-01
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
The name must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). `
|
|
34
|
+
The name is turned into the device's `source_id`, which must match `^[a-z0-9][a-z0-9._-]*$` (max 256 chars). Pass `--name` every time. Without it, and without `source_id=...` in code, the SDK makes up a random id like `source-1a2b3c4d` on first run and saves it to `~/.plexus/config.json`. Don't use the hostname: cloned SD-card images all boot as `raspberrypi`, and their telemetry merges into one source.
|
|
33
35
|
|
|
34
36
|
**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.
|
|
35
37
|
|
|
@@ -78,7 +80,7 @@ px.send_batch([
|
|
|
78
80
|
|
|
79
81
|
### `batch()` — coalesce a fast stream of readings
|
|
80
82
|
|
|
81
|
-
Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points —
|
|
83
|
+
Use this above a few readings per second. Every `send()` is one WebSocket message, and the gateway limits **messages**, not points — 2,000/s on a connection. 25 channels at 100 Hz sent one at a time is 2,500 messages/s, and the overflow is discarded before it is stored.
|
|
82
84
|
|
|
83
85
|
```python
|
|
84
86
|
with px.batch(interval_ms=50) as b:
|
|
@@ -117,10 +119,24 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
|
|
|
117
119
|
|
|
118
120
|
The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
|
|
119
121
|
|
|
122
|
+
Limits per event: a string value up to 256 bytes, a dict or list value up to 4,096 bytes of JSON, and up to 16 tags. The gateway rejects anything larger.
|
|
123
|
+
|
|
124
|
+
### Logs
|
|
125
|
+
|
|
126
|
+
There is no log-file upload and no `logging.Handler` in this package. To get important log lines into Plexus, send them as events:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
px.event("log", {"level": "error", "msg": "IMU read timed out"})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Forward the lines you would want on the timeline next to your telemetry (errors, warnings, state changes), not every debug line. Each call is one message, and the gateway limits messages (see [`batch()`](#batch--coalesce-a-fast-stream-of-readings)).
|
|
133
|
+
|
|
120
134
|
## Video streaming
|
|
121
135
|
|
|
122
136
|
Two methods depending on whether you control the capture loop or just have a URL.
|
|
123
137
|
|
|
138
|
+
Video needs a paid plan: frames go over the WebSocket, which the gateway refuses on the Free plan. Frames are relayed live to anyone watching. They are stored only when someone presses **Record** in the app, for up to 4 hours per recording.
|
|
139
|
+
|
|
124
140
|
### `send_video_frame(frame, camera_id)` — send frames you capture yourself
|
|
125
141
|
|
|
126
142
|
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.
|
|
@@ -182,10 +198,10 @@ See [`examples/`](examples/) for runnable versions of each.
|
|
|
182
198
|
|
|
183
199
|
## Reliability
|
|
184
200
|
|
|
185
|
-
Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages.
|
|
201
|
+
Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. The buffer is on disk (SQLite) by default, so it survives restarts and power loss. To keep it in memory only:
|
|
186
202
|
|
|
187
203
|
```python
|
|
188
|
-
px = Plexus(persistent_buffer=
|
|
204
|
+
px = Plexus(persistent_buffer=False)
|
|
189
205
|
```
|
|
190
206
|
|
|
191
207
|
Point counts and flush:
|
|
@@ -218,7 +234,7 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
|
|
|
218
234
|
By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — the gateway's device wire protocol. This gives you:
|
|
219
235
|
|
|
220
236
|
- lower-latency streaming of telemetry,
|
|
221
|
-
-
|
|
237
|
+
- the channel that will carry actions triggered from a Plexus dashboard.
|
|
222
238
|
|
|
223
239
|
If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
|
|
224
240
|
|
|
@@ -229,6 +245,10 @@ px = Plexus()
|
|
|
229
245
|
|
|
230
246
|
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.
|
|
231
247
|
|
|
248
|
+
Either way, plain `px.send()` is one message per call; it does not batch. `px.send_batch()` sends one list as one message, and `px.batch()` groups a fast stream for you in the background.
|
|
249
|
+
|
|
250
|
+
**On the Free plan** the gateway refuses the device WebSocket (`streaming_requires_plan`). The SDK falls back to HTTP by itself, so `send()`, `send_batch()`, `batch()` and `event()` all still work. Live streaming and video need a paid plan. Free also caps you at 3 devices and 7 days of history.
|
|
251
|
+
|
|
232
252
|
### Handling commands
|
|
233
253
|
|
|
234
254
|
Register a handler before the first `send()` so the command is advertised in the auth frame:
|
|
@@ -246,6 +266,8 @@ px.send("temperature", 72.5) # opens the socket, waits for auth
|
|
|
246
266
|
|
|
247
267
|
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).
|
|
248
268
|
|
|
269
|
+
> **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.
|
|
270
|
+
|
|
249
271
|
## Environment Variables
|
|
250
272
|
|
|
251
273
|
| Variable | Description | Default |
|
|
@@ -271,7 +293,7 @@ credentials. See [skills/README.md](skills/README.md).
|
|
|
271
293
|
## Architecture
|
|
272
294
|
|
|
273
295
|
```
|
|
274
|
-
Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
|
|
296
|
+
Your code ── px.send() ── WebSocket /ws/device (or HTTP POST /ingest) ──> plexus-gateway ──> ClickHouse + Dashboard
|
|
275
297
|
```
|
|
276
298
|
|
|
277
299
|
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.
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
`px.send()` transmits immediately: every call is its own WebSocket frame. That
|
|
4
4
|
is the right shape for a script sampling a sensor once a second, and the wrong
|
|
5
|
-
one for a test bench. The gateway allows
|
|
6
|
-
connection and
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
close to it. The dropped frames come back as `RATE_LIMITED`, after `send()` has
|
|
5
|
+
one for a test bench. The gateway allows 2,000 telemetry messages per second on
|
|
6
|
+
a connection and per source, and the ceiling counts *messages*, not points — so
|
|
7
|
+
25 channels at 100 Hz is 2,500 frames/s and lands over the limit, while the same
|
|
8
|
+
2,500 readings coalesced into ten frames is not close to it. The dropped frames come back as `RATE_LIMITED`, after `send()` has
|
|
10
9
|
already returned True, which is why this is a batching problem rather than a
|
|
11
10
|
retry one: by the time anyone can react, the points are gone.
|
|
12
11
|
|
|
@@ -182,8 +182,8 @@ class RateLimitedError(PlexusError):
|
|
|
182
182
|
# The wire slug rule (gateway validate.go sourceIDPattern, max length =
|
|
183
183
|
# MaxStringLen 256) — the old stricter local regex rejected dots, 1-char and
|
|
184
184
|
# >63-char slugs that the gateway accepts. Uuid-shaped slugs are additionally
|
|
185
|
-
# rejected
|
|
186
|
-
#
|
|
185
|
+
# rejected: the Plexus app resolves uuid-shaped refs as internal ids, which
|
|
186
|
+
# would make such a source unreachable.
|
|
187
187
|
_SOURCE_ID_RE = re.compile(r'^[a-z0-9][a-z0-9._-]*$')
|
|
188
188
|
_SOURCE_ID_UUID_RE = re.compile(
|
|
189
189
|
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
@@ -271,7 +271,7 @@ class Plexus:
|
|
|
271
271
|
if not self.api_key:
|
|
272
272
|
raise ValueError(
|
|
273
273
|
"No API key. Pass api_key=... or set PLEXUS_API_KEY. "
|
|
274
|
-
"Get a key at app.plexus.company/
|
|
274
|
+
"Get a key at app.plexus.company/api."
|
|
275
275
|
)
|
|
276
276
|
|
|
277
277
|
self.endpoint = (endpoint or get_endpoint()).rstrip("/")
|
|
@@ -372,9 +372,9 @@ class Plexus:
|
|
|
372
372
|
def _infer_class(value: FlexValue) -> str:
|
|
373
373
|
"""Numbers are metrics; everything else (str/bool/dict/list) is an event.
|
|
374
374
|
|
|
375
|
-
Mirrors the gateway
|
|
376
|
-
(
|
|
377
|
-
|
|
375
|
+
Mirrors the gateway, which infers class the same way on HTTP /ingest
|
|
376
|
+
(ingest.go). bool is a subclass of int in Python, so it must be
|
|
377
|
+
excluded explicitly or True/False would wrongly become metrics.
|
|
378
378
|
"""
|
|
379
379
|
return "metric" if isinstance(value, (int, float)) and not isinstance(value, bool) else "event"
|
|
380
380
|
|
|
@@ -543,9 +543,9 @@ class Plexus:
|
|
|
543
543
|
|
|
544
544
|
Use this whenever readings arrive faster than a few times a second.
|
|
545
545
|
`send()` puts every reading in its own WebSocket frame, and the gateway
|
|
546
|
-
limits *frames*, not points — so
|
|
547
|
-
frames/s against a
|
|
548
|
-
Batched, the same
|
|
546
|
+
limits *frames*, not points — so 25 channels at 100 Hz is 2,500
|
|
547
|
+
frames/s against a 2,000/s ceiling, and the overflow is discarded.
|
|
548
|
+
Batched, the same 2,500 readings/s is 10 frames/s.
|
|
549
549
|
|
|
550
550
|
with px.batch(interval_ms=50) as b:
|
|
551
551
|
while running:
|