plexus-python 0.11.0__tar.gz → 0.11.1__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.0 → plexus_python-0.11.1}/CHANGELOG.md +24 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/PKG-INFO +1 -1
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/__init__.py +1 -1
- {plexus_python-0.11.0 → plexus_python-0.11.1}/pyproject.toml +1 -1
- {plexus_python-0.11.0 → plexus_python-0.11.1}/skills/plexus/SKILL.md +48 -3
- {plexus_python-0.11.0 → plexus_python-0.11.1}/skills/plexus-firmware/SKILL.md +39 -2
- {plexus_python-0.11.0 → plexus_python-0.11.1}/uv.lock +1 -1
- {plexus_python-0.11.0 → plexus_python-0.11.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/.gitignore +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/AGENTS.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/API.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/CONTRIBUTING.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/LICENSE +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/README.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/SECURITY.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/TODO.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/.python-version +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/README.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/basic.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/can.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/mac_metrics.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/mavlink.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/mqtt.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/pyproject.toml +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/thermal_camera.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/examples/uv.lock +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/_log.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/batching.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/buffer.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/cameras/__init__.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/cameras/thermal.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/cli.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/client.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/config.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/plexus/ws.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/scripts/plexus.service +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/scripts/release.sh +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/scripts/scan_buses.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/scripts/setup.sh +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/scripts/verify_skills.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/skills/README.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/skills/plexus-dashboard/SKILL.md +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/conftest.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_basic.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_batching.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_batching_wire.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_buffer.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_config.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_isolation.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_rate_limit.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_retry.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_runs.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_skills.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_thermal.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_video.py +0 -0
- {plexus_python-0.11.0 → plexus_python-0.11.1}/tests/test_ws.py +0 -0
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.1] - 2026-09-02 - Correct what the agent skills teach
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`plexus-firmware` claimed the SDK batches. It does not.** Of CPython
|
|
10
|
+
targets — naming Jetson and embedded Linux specifically — the skill said
|
|
11
|
+
"stop and use the SDK instead ... It already does batching". `send()` is one
|
|
12
|
+
message per call, the gateway's ceiling counts messages, and the overflow is
|
|
13
|
+
discarded asynchronously after the send returned True. The skills are how a
|
|
14
|
+
coding agent learns this API instead of inventing it, so that line aimed
|
|
15
|
+
agents at silent data loss on exactly the hardware most likely to hit bench
|
|
16
|
+
rates. Both skills now carry the real ceilings and point at `px.batch()`.
|
|
17
|
+
|
|
18
|
+
- **Runs were documented in no skill**, so an agent had no way to know a test
|
|
19
|
+
bench should name its window. The `plexus` skill now carries the contract:
|
|
20
|
+
the app host rather than the gateway, offset-bearing timestamps, the
|
|
21
|
+
operators, and the two semantics that surprise people — every sample must
|
|
22
|
+
satisfy a criterion, and a metric with no data in the window fails rather
|
|
23
|
+
than passes.
|
|
24
|
+
|
|
25
|
+
- **The documented slug rule was wrong** (`^[a-z0-9][a-z0-9_-]{1,62}$`). The
|
|
26
|
+
gateway accepts dots, single characters and 256 bytes, so an agent applying
|
|
27
|
+
the documented rule would reject slugs the platform takes.
|
|
28
|
+
|
|
5
29
|
## [0.11.0] - 2026-09-01 - Batching, runs, and an end to silent drops
|
|
6
30
|
|
|
7
31
|
### Added
|
|
@@ -156,12 +156,54 @@ When asked to "set up Plexus" in a project, do this:
|
|
|
156
156
|
5. **Add a single usage example** in the project — one obvious place, not five — so they can verify it works.
|
|
157
157
|
6. **Update README** with a short section: "Set `PLEXUS_API_KEY`, then run X."
|
|
158
158
|
|
|
159
|
+
## Runs
|
|
160
|
+
|
|
161
|
+
A **run** is a named time window on a source — a hot-fire, a bench sequence, a
|
|
162
|
+
flight. Runs are recalled on `/runs`, compared against each other aligned at
|
|
163
|
+
T+0, and evaluated against declared pass criteria on close. Test-bench software
|
|
164
|
+
should open one when a test starts and close it when the test ends, rather than
|
|
165
|
+
expecting somebody to drag a time picker afterwards.
|
|
166
|
+
|
|
167
|
+
These live on the **app** host (`https://app.plexus.company`), not the gateway,
|
|
168
|
+
and take the same `x-api-key`.
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
POST /api/runs {name, source_id, started_at, pass_criteria?, tags?}
|
|
172
|
+
PATCH /api/runs/{id} {status: "completed"|"aborted", ended_at}
|
|
173
|
+
GET /api/runs ?source_id=&status=
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`started_at` / `ended_at` are ISO-8601 **with an offset** (`...Z` is fine); a
|
|
177
|
+
naive local timestamp is a 400.
|
|
178
|
+
|
|
179
|
+
In plexus-python (>= 0.11.0) this is `px.start_run()` / `px.end_run()`, or:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
with px.run("hotfire-03", pass_criteria=[
|
|
183
|
+
{"metric": "motor.temp_c", "operator": "<", "value": 85},
|
|
184
|
+
]):
|
|
185
|
+
bench.execute()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
An exception leaving the block closes the run `aborted`; a clean exit closes it
|
|
189
|
+
`completed`.
|
|
190
|
+
|
|
191
|
+
**Pass criteria** are `{metric, operator, value, label?}` with operator one of
|
|
192
|
+
`> >= < <= = !=`. On close, every sample of that metric inside the window is
|
|
193
|
+
checked. A criterion passes only if *all* of them satisfy it — one excursion
|
|
194
|
+
fails it — and a criterion whose metric has no data in the window **fails**
|
|
195
|
+
rather than passing. The verdict lands on the run's `test_result`.
|
|
196
|
+
|
|
197
|
+
Criteria are evaluated against stored telemetry, which trails ingest by a few
|
|
198
|
+
seconds. A run closed the instant its last point is sent may read "no data";
|
|
199
|
+
let the pipeline settle first.
|
|
200
|
+
|
|
159
201
|
## Idioms
|
|
160
202
|
|
|
161
203
|
- **Polling cadences for dashboards**: latest values 5s, charts 10s, fleet health 10s, source list 30s. Use SWR or TanStack Query with `refreshInterval`.
|
|
162
204
|
- **Time ranges**: prefer `last=1h` (relative) over `start`/`end` (absolute) — easier to reason about and less timezone footgun. `start`/`end` are ISO date-times, not epoch ms.
|
|
163
|
-
- **Batching ingest**:
|
|
164
|
-
- **Source IDs are slugs**: `drone-001`, `sat-alpha-3`. Must match `^[a-z0-9][a-z0-
|
|
205
|
+
- **Batching ingest**: the gateway meters **messages, not points** — 500/s per WebSocket connection, 2000/s per source, up to 10,000 points in one message. Buffer up to 64 points or 5 seconds, whichever first. One send per reading at bench rates exceeds the limit and the overflow is *discarded*, reported asynchronously as `RATE_LIMITED` after the send returned. In plexus-python use `px.batch(interval_ms=50)`; `px.send()` alone is one message per call. On 429 / 5xx, exponential backoff with max 3 attempts.
|
|
206
|
+
- **Source IDs are slugs**: `drone-001`, `sat-alpha-3`, `bench.rig-2`. Must match `^[a-z0-9][a-z0-9._-]*$`, up to 256 characters — dots are legal and a single character is legal. A uuid-shaped slug is rejected: every resolver reads uuid-shaped refs as internal ids, so such a source would be unreachable. Stable, lowercase.
|
|
165
207
|
- **Source IDs are not deduplicated.** The gateway writes whatever `source_id` you declare. Two devices declaring the same name merge into one source.
|
|
166
208
|
- **Metric names are opaque and may be long.** Anything that round-trips a name must use the identical string on both sides or the series and its metadata will not join.
|
|
167
209
|
|
|
@@ -180,4 +222,7 @@ When asked to "set up Plexus" in a project, do this:
|
|
|
180
222
|
|
|
181
223
|
Fetch `https://plexus-data-api.fly.dev/openapi.json` for the authoritative HTTP schema (it will not show WebSocket routes), and check a real response before writing parsing code. `scripts/verify_skills.py` in this repo checks these docs against the live spec — run it if something here looks stale.
|
|
182
224
|
|
|
183
|
-
This cheat sheet has drifted before. Corrected 2026-
|
|
225
|
+
This cheat sheet has drifted before. Corrected 2026-09-01 against the shipped
|
|
226
|
+
gateway and app (runs API — which an earlier revision of API.md wrongly said did
|
|
227
|
+
not exist; message-not-point metering and `px.batch()`; the real slug rule,
|
|
228
|
+
which permits dots, single characters and 256 bytes). Corrected 2026-08-27 against Data API 0.1.0 (ingest array name, `sources`/`devices` paths, removed per-source health, columnar query) and again 2026-08-28 against gateway + API source (the live-stream host/path/auth/frame shape, the required `class` field, numeric-only timestamps, the redirect chain, and the now-fixed 1970 timestamp behavior).
|
|
@@ -14,11 +14,29 @@ Adds a small, well-behaved Plexus ingest client to device-side code. Optimized f
|
|
|
14
14
|
- Targets include: ESP32 / Arduino / RP2040, Raspberry Pi, NVIDIA Jetson, embedded Linux gateways, drone autopilot companion computers, satellite OBC software
|
|
15
15
|
- Languages: C / C++, MicroPython, CPython, Rust, Go (for edge gateways)
|
|
16
16
|
|
|
17
|
-
**If the target runs CPython (Pi, Jetson, any embedded Linux with room), stop and use the SDK instead** — `pip install plexus-python`, then `Plexus(api_key=..., source_id=...)
|
|
17
|
+
**If the target runs CPython (Pi, Jetson, any embedded Linux with room), stop and use the SDK instead** — `pip install plexus-python`, then `Plexus(api_key=..., source_id=...)`. It handles backoff, store-and-forward buffering and the WebSocket transport. Hand-rolling is for targets the SDK can't reach.
|
|
18
|
+
|
|
19
|
+
**`send()` does NOT batch.** Every call is its own message on the wire, and the ceiling below counts messages. Above a few readings a second, use `px.batch()`:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
with px.batch(interval_ms=50) as b: # one message per interval
|
|
23
|
+
while running:
|
|
24
|
+
b.send("imu.accel_x", imu.x) # same signature as px.send()
|
|
25
|
+
b.send("imu.accel_y", imu.y)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Leaving the block flushes what is queued, and readings keep the timestamp they were taken at. Requires plexus-python >= 0.11.0.
|
|
18
29
|
|
|
19
30
|
For **server-side ingestion** (a backend collecting metrics and forwarding), the generic `plexus` skill is fine.
|
|
20
31
|
For **dashboards / UI**, use `plexus-dashboard`.
|
|
21
32
|
|
|
33
|
+
**On a test bench, open a run.** Software driving a test should name its window
|
|
34
|
+
rather than leave someone to find it on a chart later: `POST /api/runs` when the
|
|
35
|
+
test starts, `PATCH /api/runs/{id}` with `status` and `ended_at` when it ends
|
|
36
|
+
(app host, same `x-api-key`), or `with px.run(...)` from plexus-python. Declared
|
|
37
|
+
`pass_criteria` are evaluated against every sample in the window on close. See
|
|
38
|
+
the `plexus` skill for the contract.
|
|
39
|
+
|
|
22
40
|
## The ingest contract
|
|
23
41
|
|
|
24
42
|
```
|
|
@@ -53,7 +71,26 @@ Every firmware integration must do these. Skip any of them and you'll lose data
|
|
|
53
71
|
|
|
54
72
|
### 1. Batch
|
|
55
73
|
|
|
56
|
-
|
|
74
|
+
**The gateway meters messages, not points.** One request carrying 200 points costs
|
|
75
|
+
exactly what one carrying 1 costs, so the shape of your sends — not their volume —
|
|
76
|
+
decides whether you hit a limit:
|
|
77
|
+
|
|
78
|
+
| Limit | Value |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| Telemetry messages per WebSocket connection | 500/s sustained, 2000 burst |
|
|
81
|
+
| Hard ceiling per source (WS and HTTP) | 2000 messages/s |
|
|
82
|
+
| Points per message | 10,000 |
|
|
83
|
+
| Body size | 1 MB (WS) / 5 MB (HTTP) |
|
|
84
|
+
|
|
85
|
+
Eight channels at 100 Hz sent one at a time is 800 messages/s — over the limit,
|
|
86
|
+
and **the overflow is discarded**. Batched every 50 ms it is 20 messages/s.
|
|
87
|
+
|
|
88
|
+
Over the limit the gateway drops the whole message and answers `RATE_LIMITED`,
|
|
89
|
+
which arrives *after* the send returned. Those points cannot be recovered, so
|
|
90
|
+
this is a batching problem, not a retry one.
|
|
91
|
+
|
|
92
|
+
Never POST one point at a time. Buffer until **64 points OR 5 seconds**, whichever
|
|
93
|
+
first, then flush. Constants worth exposing:
|
|
57
94
|
|
|
58
95
|
```c
|
|
59
96
|
#define PLEXUS_BATCH_SIZE 64
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|