plexus-python 0.8.0__tar.gz → 0.9.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.8.0 → plexus_python-0.9.1}/.github/workflows/publish.yml +17 -3
- {plexus_python-0.8.0 → plexus_python-0.9.1}/AGENTS.md +15 -10
- {plexus_python-0.8.0 → plexus_python-0.9.1}/API.md +41 -149
- {plexus_python-0.8.0 → plexus_python-0.9.1}/CHANGELOG.md +53 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/PKG-INFO +10 -21
- {plexus_python-0.8.0 → plexus_python-0.9.1}/README.md +7 -18
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/mac_metrics.py +2 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/thermal_camera.py +1 -2
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/uv.lock +4 -4
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/__init__.py +3 -3
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/buffer.py +17 -16
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/cameras/thermal.py +6 -6
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/cli.py +7 -9
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/client.py +199 -142
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/config.py +3 -4
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/ws.py +36 -29
- {plexus_python-0.8.0 → plexus_python-0.9.1}/pyproject.toml +2 -2
- {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/release.sh +13 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/scan_buses.py +1 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_basic.py +1 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_buffer.py +0 -2
- {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_config.py +1 -2
- {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_retry.py +102 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_video.py +0 -1
- {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_ws.py +8 -8
- {plexus_python-0.8.0 → plexus_python-0.9.1}/uv.lock +92 -96
- {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/.gitignore +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/CONTRIBUTING.md +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/LICENSE +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/SECURITY.md +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/TODO.md +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/.python-version +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/README.md +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/basic.py +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/can.py +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/mavlink.py +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/mqtt.py +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/examples/pyproject.toml +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/_log.py +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/plexus/cameras/__init__.py +2 -2
- {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/plexus.service +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/scripts/setup.sh +0 -0
- {plexus_python-0.8.0 → plexus_python-0.9.1}/tests/test_thermal.py +0 -0
|
@@ -30,10 +30,24 @@ jobs:
|
|
|
30
30
|
match = re.search(r'^version\s*=\s*\"(.+?)\"', f.read(), re.MULTILINE)
|
|
31
31
|
print(match.group(1))
|
|
32
32
|
")
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
INIT_VERSION=$(python -c "
|
|
34
|
+
import re
|
|
35
|
+
with open('plexus/__init__.py') as f:
|
|
36
|
+
match = re.search(r'^__version__\s*=\s*\"(.+?)\"', f.read(), re.MULTILINE)
|
|
37
|
+
print(match.group(1))
|
|
38
|
+
")
|
|
39
|
+
echo "Git tag version: $TAG"
|
|
40
|
+
echo "pyproject.toml: $CODE_VERSION"
|
|
41
|
+
echo "plexus/__init__.py: $INIT_VERSION"
|
|
35
42
|
if [ "$TAG" != "$CODE_VERSION" ]; then
|
|
36
|
-
echo "::error::Tag v$TAG does not match
|
|
43
|
+
echo "::error::Tag v$TAG does not match pyproject.toml version $CODE_VERSION"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
# 0.9.0 shipped to PyPI with __version__ still reading 0.8.0: the CI
|
|
47
|
+
# job that checks this does not gate publish, and this job only read
|
|
48
|
+
# pyproject.toml. Check it here, where it can actually stop an upload.
|
|
49
|
+
if [ "$TAG" != "$INIT_VERSION" ]; then
|
|
50
|
+
echo "::error::Tag v$TAG does not match plexus/__init__.py version $INIT_VERSION"
|
|
37
51
|
exit 1
|
|
38
52
|
fi
|
|
39
53
|
|
|
@@ -4,22 +4,27 @@ Machine-readable interface for AI assistants and automation scripts.
|
|
|
4
4
|
|
|
5
5
|
## Environment Variables
|
|
6
6
|
|
|
7
|
-
| Variable | Description
|
|
8
|
-
| ----------------------- |
|
|
9
|
-
| `PLEXUS_API_KEY` | API key for authentication (required)
|
|
10
|
-
| `PLEXUS_GATEWAY_URL` | Gateway HTTP ingest URL
|
|
11
|
-
| `PLEXUS_GATEWAY_WS_URL` | Gateway WebSocket URL
|
|
7
|
+
| Variable | Description | Default |
|
|
8
|
+
| ----------------------- | ----------------------------------------------- | -------------------------------- |
|
|
9
|
+
| `PLEXUS_API_KEY` | API key for authentication (required) | none |
|
|
10
|
+
| `PLEXUS_GATEWAY_URL` | Gateway HTTP ingest URL | `https://gateway.plexus.company` |
|
|
11
|
+
| `PLEXUS_GATEWAY_WS_URL` | Gateway WebSocket URL | `wss://gateway.plexus.company` |
|
|
12
|
+
| `PLEXUS_ENDPOINT` | Product app URL (runs, frame uploads, CLI auth) | `https://app.plexus.company` |
|
|
13
|
+
| `PLEXUS_QUIET` | Set `1`/`true`/`yes` to silence `[plexus]` stderr status lines | unset (status lines on) |
|
|
12
14
|
|
|
13
15
|
## CLI Commands
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
plexus
|
|
17
|
-
plexus
|
|
18
|
-
plexus
|
|
19
|
-
plexus
|
|
18
|
+
plexus init # Authorize this machine, save an API key locally (alias: login)
|
|
19
|
+
plexus init --name my-key # Label for the issued key (default: cli-<hostname>)
|
|
20
|
+
plexus init --force # Overwrite an existing local key
|
|
21
|
+
plexus logout # Forget the local API key
|
|
22
|
+
plexus whoami # Show the local credential summary
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
`plexus init` opens a browser to `app.plexus.company/auth/cli`, waits for the callback, and
|
|
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.
|
|
23
28
|
|
|
24
29
|
## Exit Codes
|
|
25
30
|
|
|
@@ -55,8 +55,8 @@ Plexus uses API keys for all authentication:
|
|
|
55
55
|
|
|
56
56
|
**Option A: CLI setup (recommended for devices)**
|
|
57
57
|
|
|
58
|
-
1. Run `plexus
|
|
59
|
-
2.
|
|
58
|
+
1. Run `plexus init` on your device
|
|
59
|
+
2. Authorize the machine in the browser tab it opens
|
|
60
60
|
3. API key is saved to `~/.plexus/config.json`
|
|
61
61
|
|
|
62
62
|
**Option B: Manual creation**
|
|
@@ -77,7 +77,7 @@ x-api-key: plx_xxxxx
|
|
|
77
77
|
|
|
78
78
|
### Send Data
|
|
79
79
|
|
|
80
|
-
**POST**
|
|
80
|
+
**POST** `https://gateway.plexus.company/ingest`
|
|
81
81
|
|
|
82
82
|
```json
|
|
83
83
|
{
|
|
@@ -87,8 +87,7 @@ x-api-key: plx_xxxxx
|
|
|
87
87
|
"value": 72.5,
|
|
88
88
|
"timestamp": 1699900000.123,
|
|
89
89
|
"source_id": "sensor-001",
|
|
90
|
-
"tags": { "location": "lab" }
|
|
91
|
-
"session_id": "test-001"
|
|
90
|
+
"tags": { "location": "lab" }
|
|
92
91
|
}
|
|
93
92
|
]
|
|
94
93
|
}
|
|
@@ -101,7 +100,6 @@ x-api-key: plx_xxxxx
|
|
|
101
100
|
| `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). |
|
|
102
101
|
| `source_id` | string | Yes | Your source identifier |
|
|
103
102
|
| `tags` | object | No | Key-value labels |
|
|
104
|
-
| `session_id` | string | No | Group data into sessions |
|
|
105
103
|
|
|
106
104
|
### Supported Value Types
|
|
107
105
|
|
|
@@ -115,29 +113,7 @@ x-api-key: plx_xxxxx
|
|
|
115
113
|
|
|
116
114
|
### Sessions
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
**Create session:**
|
|
121
|
-
|
|
122
|
-
```json
|
|
123
|
-
POST /api/sessions
|
|
124
|
-
{
|
|
125
|
-
"session_id": "test-001",
|
|
126
|
-
"name": "Motor Test Run",
|
|
127
|
-
"source_id": "sensor-001",
|
|
128
|
-
"status": "active"
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**End session:**
|
|
133
|
-
|
|
134
|
-
```json
|
|
135
|
-
PATCH /api/sessions/{session_id}
|
|
136
|
-
{
|
|
137
|
-
"status": "completed",
|
|
138
|
-
"ended_at": "2024-01-15T10:30:00Z"
|
|
139
|
-
}
|
|
140
|
-
```
|
|
116
|
+
> **Removed / not built.** There is no sessions or runs REST API (`POST /api/sessions`, `POST /api/runs`) — no such route exists in the gateway or platform, and the SDK's former `run()` context has been removed. To group a slice of data, use plain `tags` on each point.
|
|
141
117
|
|
|
142
118
|
## WebSocket API
|
|
143
119
|
|
|
@@ -146,13 +122,13 @@ For real-time UI-controlled streaming, devices connect via WebSocket.
|
|
|
146
122
|
### Connection Flow
|
|
147
123
|
|
|
148
124
|
1. Device connects to the gateway
|
|
149
|
-
2. Device authenticates with API key
|
|
150
|
-
3. Device
|
|
151
|
-
4. Dashboard
|
|
125
|
+
2. Device authenticates with API key (and advertises any registered commands)
|
|
126
|
+
3. Device streams `telemetry` frames
|
|
127
|
+
4. Dashboard/API invokes registered commands via `typed_command`
|
|
152
128
|
|
|
153
129
|
### Device Authentication
|
|
154
130
|
|
|
155
|
-
Devices authenticate using an API key. The
|
|
131
|
+
Devices authenticate using an API key. The gateway echoes the declared `source_id` back unchanged — there is no auto-suffixing, so pick a unique `source_id` per device.
|
|
156
132
|
|
|
157
133
|
```json
|
|
158
134
|
// Device → Server
|
|
@@ -160,9 +136,8 @@ Devices authenticate using an API key. The `source_id` in the request is the dev
|
|
|
160
136
|
"type": "device_auth",
|
|
161
137
|
"api_key": "plx_xxxxx",
|
|
162
138
|
"source_id": "drone-01",
|
|
163
|
-
"install_id": "c9f2e0b46f4a4f6a8c3e1d5b0a2e7f91",
|
|
164
139
|
"platform": "python-sdk",
|
|
165
|
-
"agent_version": "0.
|
|
140
|
+
"agent_version": "0.8.0"
|
|
166
141
|
}
|
|
167
142
|
|
|
168
143
|
// Server → Device
|
|
@@ -171,53 +146,34 @@ Devices authenticate using an API key. The `source_id` in the request is the dev
|
|
|
171
146
|
"source_id": "drone-01",
|
|
172
147
|
"server_time_ms": 1746100800000
|
|
173
148
|
}
|
|
174
|
-
|
|
175
|
-
// Server → Device (collision case)
|
|
176
|
-
{
|
|
177
|
-
"type": "authenticated",
|
|
178
|
-
"source_id": "drone-01_2",
|
|
179
|
-
"server_time_ms": 1746100800000
|
|
180
|
-
}
|
|
181
149
|
```
|
|
182
150
|
|
|
183
|
-
The SDK **adopts** whatever `source_id` the server returns and uses it for all subsequent frames, heartbeats, and reconnects. It also persists the assigned name locally so reconnects go straight to the claimed slot.
|
|
184
|
-
|
|
185
151
|
`server_time_ms` is the gateway's current Unix time in milliseconds. The Python SDK uses it to compute a clock offset (`server_time - device_time`) that is applied to every SDK-generated timestamp for the lifetime of the connection. This corrects for devices that boot without NTP or have an unreliable RTC — a common condition on embedded Linux. See [Clock correction](#clock-correction) for details and limitations.
|
|
186
152
|
|
|
187
|
-
`install_id`
|
|
153
|
+
> **Removed:** `install_id` and server-side `source_id` auto-suffixing were removed in 0.7.1. The device_auth frame no longer carries an `install_id`, and the client does not adopt or persist a server-assigned name.
|
|
188
154
|
|
|
189
|
-
###
|
|
155
|
+
### Frame Types
|
|
190
156
|
|
|
191
|
-
|
|
192
|
-
| --------------- | ------------------------------------ |
|
|
193
|
-
| `start_stream` | Start streaming sensor data |
|
|
194
|
-
| `stop_stream` | Stop streaming |
|
|
195
|
-
| `start_session` | Start recording to a session |
|
|
196
|
-
| `stop_session` | Stop recording |
|
|
197
|
-
| `configure` | Configure sensor (e.g., sample rate) |
|
|
198
|
-
| `ping` | Keepalive request |
|
|
157
|
+
**Device → Server**
|
|
199
158
|
|
|
200
|
-
|
|
159
|
+
| Type | Description |
|
|
160
|
+
| ---------------- | ----------------------------------------------- |
|
|
161
|
+
| `device_auth` | Authenticate on connect (see above) |
|
|
162
|
+
| `telemetry` | Sensor data points |
|
|
163
|
+
| `heartbeat` | Liveness ping (every 30s) |
|
|
164
|
+
| `command_result` | `ack` / `result` / `error` for a typed command |
|
|
201
165
|
|
|
202
|
-
|
|
203
|
-
| ----------------- | ----------------------- |
|
|
204
|
-
| `telemetry` | Sensor data points |
|
|
205
|
-
| `session_started` | Confirm session started |
|
|
206
|
-
| `session_stopped` | Confirm session stopped |
|
|
207
|
-
| `pong` | Keepalive response |
|
|
166
|
+
**Server → Device**
|
|
208
167
|
|
|
209
|
-
|
|
168
|
+
| Type | Description |
|
|
169
|
+
| --------------- | ----------------------------------------------------- |
|
|
170
|
+
| `authenticated` | Auth accepted; carries `server_time_ms` |
|
|
171
|
+
| `typed_command` | Invoke a command the device registered via `on_command` |
|
|
210
172
|
|
|
211
|
-
|
|
212
|
-
// Dashboard → Device
|
|
213
|
-
{
|
|
214
|
-
"type": "start_stream",
|
|
215
|
-
"source_id": "my-device-001",
|
|
216
|
-
"metrics": ["accel_x", "accel_y", "accel_z"],
|
|
217
|
-
"interval_ms": 100
|
|
218
|
-
}
|
|
173
|
+
### Telemetry
|
|
219
174
|
|
|
220
|
-
|
|
175
|
+
```json
|
|
176
|
+
// Device → Server (streamed continuously)
|
|
221
177
|
{
|
|
222
178
|
"type": "telemetry",
|
|
223
179
|
"points": [
|
|
@@ -228,54 +184,20 @@ The SDK **adopts** whatever `source_id` the server returns and uses it for all s
|
|
|
228
184
|
}
|
|
229
185
|
```
|
|
230
186
|
|
|
231
|
-
###
|
|
187
|
+
### Commands
|
|
232
188
|
|
|
233
|
-
|
|
234
|
-
// Dashboard → Device
|
|
235
|
-
{
|
|
236
|
-
"type": "start_session",
|
|
237
|
-
"source_id": "my-device-001",
|
|
238
|
-
"session_id": "session_1699900000_abc123",
|
|
239
|
-
"session_name": "Motor Test",
|
|
240
|
-
"metrics": [],
|
|
241
|
-
"interval_ms": 100
|
|
242
|
-
}
|
|
189
|
+
Dashboard/API actions 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()`.
|
|
243
190
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"session_id": "session_1699900000_abc123",
|
|
248
|
-
"session_name": "Motor Test"
|
|
249
|
-
}
|
|
191
|
+
```json
|
|
192
|
+
// Server → Device
|
|
193
|
+
{ "type": "typed_command", "id": "cmd-1", "command": "reboot", "params": { "delay_s": 0 } }
|
|
250
194
|
|
|
251
|
-
// Device
|
|
252
|
-
{
|
|
253
|
-
|
|
254
|
-
"session_id": "session_1699900000_abc123",
|
|
255
|
-
"points": [
|
|
256
|
-
{
|
|
257
|
-
"metric": "accel_x",
|
|
258
|
-
"value": 0.12,
|
|
259
|
-
"timestamp": 1699900000123,
|
|
260
|
-
"tags": { "session_id": "session_1699900000_abc123" }
|
|
261
|
-
}
|
|
262
|
-
]
|
|
263
|
-
}
|
|
195
|
+
// Device → Server (ack, then result or error)
|
|
196
|
+
{ "type": "command_result", "id": "cmd-1", "command": "reboot", "event": "ack" }
|
|
197
|
+
{ "type": "command_result", "id": "cmd-1", "command": "reboot", "event": "result", "result": { "ok": true } }
|
|
264
198
|
```
|
|
265
199
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
```json
|
|
269
|
-
// Dashboard → Device
|
|
270
|
-
{
|
|
271
|
-
"type": "configure",
|
|
272
|
-
"source_id": "my-device-001",
|
|
273
|
-
"sensor": "MPU6050",
|
|
274
|
-
"config": {
|
|
275
|
-
"sample_rate": 50
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
```
|
|
200
|
+
> **Removed / not built.** There are no raw `start_stream`, `stop_stream`, `start_session`, `stop_session`, `configure`, `session_started`, or `session_stopped` frames — the earlier agent-style streaming/recording protocol was removed. Dashboard-driven control now flows through the `typed_command` envelope above.
|
|
279
201
|
|
|
280
202
|
## Code Examples
|
|
281
203
|
|
|
@@ -432,36 +354,7 @@ while True:
|
|
|
432
354
|
|
|
433
355
|
## Python SDK with Sensor Drivers
|
|
434
356
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
```bash
|
|
438
|
-
pip install plexus-python[sensors]
|
|
439
|
-
plexus start
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
### Supported Sensors
|
|
443
|
-
|
|
444
|
-
| Sensor | Type | Metrics | I2C Address |
|
|
445
|
-
| ------- | ----------- | ------------------------------------------------------------- | ----------- |
|
|
446
|
-
| MPU6050 | 6-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68, 0x69 |
|
|
447
|
-
| MPU9250 | 9-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68 |
|
|
448
|
-
| BME280 | Environment | `temperature`, `humidity`, `pressure` | 0x76, 0x77 |
|
|
449
|
-
|
|
450
|
-
### Custom Sensors
|
|
451
|
-
|
|
452
|
-
```python
|
|
453
|
-
from plexus.sensors import BaseSensor, SensorReading
|
|
454
|
-
|
|
455
|
-
class MySensor(BaseSensor):
|
|
456
|
-
name = "MySensor"
|
|
457
|
-
metrics = ["voltage", "current"]
|
|
458
|
-
|
|
459
|
-
def read(self):
|
|
460
|
-
return [
|
|
461
|
-
SensorReading("voltage", read_adc(0) * 3.3),
|
|
462
|
-
SensorReading("current", read_adc(1) * 0.1),
|
|
463
|
-
]
|
|
464
|
-
```
|
|
357
|
+
> **Removed / not built.** There is no `plexus-python[sensors]` extra and no `plexus.sensors` module (`BaseSensor` / `SensorReading` do not exist). The only optional extras are `[video]` and `[dev]`. This SDK stays out of your decode path — read your sensor with whatever library you already use and pass values to `px.send()` (see [Bring Your Own Protocol](#bring-your-own-protocol) above).
|
|
465
358
|
|
|
466
359
|
## Errors
|
|
467
360
|
|
|
@@ -503,14 +396,13 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is
|
|
|
503
396
|
**Known limitations:**
|
|
504
397
|
|
|
505
398
|
- The clock offset refreshes only on WebSocket reconnect. A device with a drifting RTC that stays connected for many days will accumulate uncorrected drift between reconnects proportional to the drift rate.
|
|
506
|
-
- HTTP
|
|
507
|
-
- `send_batch()` takes one shared `timestamp`
|
|
399
|
+
- The HTTP fallback path (used when the WebSocket is unavailable) does not receive clock sync — timestamps default to the device clock uncorrected.
|
|
400
|
+
- `send_batch()` takes one shared `timestamp` by default; pass `(metric, value, timestamp)` 3-tuples for per-point timestamps.
|
|
508
401
|
|
|
509
402
|
## Best Practices
|
|
510
403
|
|
|
511
404
|
- **Batch points** - Send up to 100 points per request for HTTP
|
|
512
405
|
- **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
|
|
513
406
|
- **Consistent source_id** - Use the same ID for each physical device/source
|
|
514
|
-
- **Use tags** - Label data for filtering (e.g., `{"location": "lab"}`)
|
|
515
|
-
- **
|
|
516
|
-
- **Prefer WebSocket** - For real-time UI-controlled devices, use `plexus start`
|
|
407
|
+
- **Use tags** - Label data for filtering and grouping (e.g., `{"location": "lab"}`)
|
|
408
|
+
- **Prefer WebSocket** - For real-time UI-controlled devices, the SDK connects over WebSocket by default
|
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.9.1] - 2026-08-27 - Version sync
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `plexus.__version__` reported `0.8.0` in the 0.9.0 release. The release script
|
|
10
|
+
and the publish workflow both validated `pyproject.toml` only; the CI job that
|
|
11
|
+
compares the two version sites does not gate publishing, so it failed after the
|
|
12
|
+
upload had already gone out. Both gates now check `plexus/__init__.py` too.
|
|
13
|
+
0.9.0 is functionally identical to this release — only the reported version
|
|
14
|
+
string was wrong.
|
|
15
|
+
|
|
16
|
+
## [0.9.0] - 2026-08-27 - Flexible values and the real slug rule
|
|
17
|
+
|
|
18
|
+
Two fixes for data that looked like it was sent but never arrived, plus the
|
|
19
|
+
removal of a dead API.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`send()` now infers a point's class from its value type** instead of always
|
|
24
|
+
tagging `class="metric"`. Numbers stay metrics; strings, bools, dicts and
|
|
25
|
+
lists become events. The gateway rejects a non-numeric value on
|
|
26
|
+
`class="metric"` and drops the *whole frame* — so `px.send("state", "RUNNING")`
|
|
27
|
+
silently took every other point in that batch down with it. Non-numeric values
|
|
28
|
+
have been advertised since 0.2.0; this is what makes them work.
|
|
29
|
+
An explicit `data_class="metric"` with a non-numeric value now raises
|
|
30
|
+
`PlexusError` locally rather than failing on the wire.
|
|
31
|
+
|
|
32
|
+
- **`source_id` validation matched an older, stricter rule than the server.**
|
|
33
|
+
The SDK enforced `^[a-z0-9][a-z0-9_-]{1,62}$` — no dots, minimum two
|
|
34
|
+
characters — while the gateway accepts `^[a-z0-9][a-z0-9._-]*$` up to 256
|
|
35
|
+
characters. Names production would have accepted (`rack.01`, `a`) raised
|
|
36
|
+
`ValueError` in the constructor before a single byte left the host, which
|
|
37
|
+
reads as an auth problem and is not one. The SDK now mirrors the wire rule.
|
|
38
|
+
Uuid-shaped ids are still rejected: the app resolves those as internal ids,
|
|
39
|
+
so such a source would be unreachable.
|
|
40
|
+
|
|
41
|
+
- Buffer drains are chunked, so a backlog over the gateway's 10k-points-per-batch
|
|
42
|
+
ceiling no longer wedges every subsequent send. The WebSocket auth wait is paid
|
|
43
|
+
once, on the first send, instead of stalling each call.
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
|
|
47
|
+
- `run()` context manager and its `POST /api/runs` plumbing. The `/api/runs` route never
|
|
48
|
+
existed on the platform (both notifications failed silently on every call), the ingest
|
|
49
|
+
loader drops `run_id` from points, and nothing was ever stored — so removal loses no
|
|
50
|
+
data. To group a slice of data, use `tags` on each point.
|
|
51
|
+
|
|
52
|
+
### Security
|
|
53
|
+
|
|
54
|
+
- Bumped pillow to 12.3.0 and idna to 3.19 for published advisories.
|
|
55
|
+
|
|
3
56
|
## [0.8.0] - 2026-07-02 - Command concurrency control
|
|
4
57
|
|
|
5
58
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: plexus-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -27,7 +27,7 @@ Requires-Dist: numpy>=1.24; extra == 'dev'
|
|
|
27
27
|
Requires-Dist: opencv-python-headless>=4.8; extra == 'dev'
|
|
28
28
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
29
29
|
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
30
|
-
Requires-Dist: ruff; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff==0.15.12; extra == 'dev'
|
|
31
31
|
Requires-Dist: websockets>=12; extra == 'dev'
|
|
32
32
|
Provides-Extra: video
|
|
33
33
|
Requires-Dist: numpy>=1.24; extra == 'video'
|
|
@@ -68,7 +68,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
|
|
|
68
68
|
|
|
69
69
|
The name must match `^[a-z0-9][a-z0-9_-]{1,62}$`. `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`.
|
|
70
70
|
|
|
71
|
-
**
|
|
71
|
+
**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
72
|
|
|
73
73
|
In normal code, you usually just pass `source_id=...` explicitly to `Plexus(...)` and never have to think about it.
|
|
74
74
|
|
|
@@ -111,7 +111,7 @@ px.send_batch([
|
|
|
111
111
|
])
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
`points` is a list of `(metric, value)` tuples.
|
|
114
|
+
`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`).
|
|
115
115
|
|
|
116
116
|
### `event(name, data)` — record a discrete occurrence
|
|
117
117
|
|
|
@@ -125,16 +125,6 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
|
|
|
125
125
|
|
|
126
126
|
The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
|
|
127
127
|
|
|
128
|
-
### `run(run_id)` — group data into a named recording
|
|
129
|
-
|
|
130
|
-
```python
|
|
131
|
-
with px.run("thermal-cycle-001"):
|
|
132
|
-
while running:
|
|
133
|
-
px.send("temperature", read_temp())
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
All `send()` calls inside the context are tagged with `run_id`, making it easy to isolate and replay that slice of data in the dashboard.
|
|
137
|
-
|
|
138
128
|
## Video streaming
|
|
139
129
|
|
|
140
130
|
Two methods depending on whether you control the capture loop or just have a URL.
|
|
@@ -228,8 +218,8 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
|
|
|
228
218
|
|
|
229
219
|
**Known limits:**
|
|
230
220
|
- Clock sync refreshes on WebSocket (re)connect. A device with a drifting RTC that stays connected for many days accumulates uncorrected drift between reconnects.
|
|
231
|
-
- HTTP
|
|
232
|
-
- `send_batch()` shares one timestamp across the
|
|
221
|
+
- The HTTP fallback path (used when the WebSocket is unavailable) does not receive clock sync — timestamps default to the uncorrected device clock.
|
|
222
|
+
- `send_batch()` shares one timestamp across the batch by default; pass `(metric, value, timestamp)` 3-tuples for per-point timestamps.
|
|
233
223
|
|
|
234
224
|
## Transport
|
|
235
225
|
|
|
@@ -241,13 +231,12 @@ By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway
|
|
|
241
231
|
If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
|
|
242
232
|
|
|
243
233
|
```python
|
|
244
|
-
#
|
|
234
|
+
# ws with transparent http fallback — this is the only mode
|
|
245
235
|
px = Plexus()
|
|
246
|
-
|
|
247
|
-
# force http (legacy)
|
|
248
|
-
px = Plexus(transport="http")
|
|
249
236
|
```
|
|
250
237
|
|
|
238
|
+
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.
|
|
239
|
+
|
|
251
240
|
### Handling commands
|
|
252
241
|
|
|
253
242
|
Register a handler before the first `send()` so the command is advertised in the auth frame:
|
|
@@ -31,7 +31,7 @@ curl -sL https://app.plexus.company/setup | bash -s -- \
|
|
|
31
31
|
|
|
32
32
|
The name must match `^[a-z0-9][a-z0-9_-]{1,62}$`. `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`.
|
|
33
33
|
|
|
34
|
-
**
|
|
34
|
+
**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
35
|
|
|
36
36
|
In normal code, you usually just pass `source_id=...` explicitly to `Plexus(...)` and never have to think about it.
|
|
37
37
|
|
|
@@ -74,7 +74,7 @@ px.send_batch([
|
|
|
74
74
|
])
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
`points` is a list of `(metric, value)` tuples.
|
|
77
|
+
`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`).
|
|
78
78
|
|
|
79
79
|
### `event(name, data)` — record a discrete occurrence
|
|
80
80
|
|
|
@@ -88,16 +88,6 @@ px.event("sensor_error", {"sensor": "imu", "code": 42}, tags={"motor": "A"})
|
|
|
88
88
|
|
|
89
89
|
The platform displays events as markers overlaid on your telemetry charts, not as time-series lines.
|
|
90
90
|
|
|
91
|
-
### `run(run_id)` — group data into a named recording
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
with px.run("thermal-cycle-001"):
|
|
95
|
-
while running:
|
|
96
|
-
px.send("temperature", read_temp())
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
All `send()` calls inside the context are tagged with `run_id`, making it easy to isolate and replay that slice of data in the dashboard.
|
|
100
|
-
|
|
101
91
|
## Video streaming
|
|
102
92
|
|
|
103
93
|
Two methods depending on whether you control the capture loop or just have a URL.
|
|
@@ -191,8 +181,8 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no cor
|
|
|
191
181
|
|
|
192
182
|
**Known limits:**
|
|
193
183
|
- Clock sync refreshes on WebSocket (re)connect. A device with a drifting RTC that stays connected for many days accumulates uncorrected drift between reconnects.
|
|
194
|
-
- HTTP
|
|
195
|
-
- `send_batch()` shares one timestamp across the
|
|
184
|
+
- The HTTP fallback path (used when the WebSocket is unavailable) does not receive clock sync — timestamps default to the uncorrected device clock.
|
|
185
|
+
- `send_batch()` shares one timestamp across the batch by default; pass `(metric, value, timestamp)` 3-tuples for per-point timestamps.
|
|
196
186
|
|
|
197
187
|
## Transport
|
|
198
188
|
|
|
@@ -204,13 +194,12 @@ By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway
|
|
|
204
194
|
If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
|
|
205
195
|
|
|
206
196
|
```python
|
|
207
|
-
#
|
|
197
|
+
# ws with transparent http fallback — this is the only mode
|
|
208
198
|
px = Plexus()
|
|
209
|
-
|
|
210
|
-
# force http (legacy)
|
|
211
|
-
px = Plexus(transport="http")
|
|
212
199
|
```
|
|
213
200
|
|
|
201
|
+
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.
|
|
202
|
+
|
|
214
203
|
### Handling commands
|
|
215
204
|
|
|
216
205
|
Register a handler before the first `send()` so the command is advertised in the auth frame:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
3
3
|
requires-python = ">=3.12"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
@@ -389,11 +389,11 @@ wheels = [
|
|
|
389
389
|
|
|
390
390
|
[[package]]
|
|
391
391
|
name = "idna"
|
|
392
|
-
version = "3.
|
|
392
|
+
version = "3.19"
|
|
393
393
|
source = { registry = "https://pypi.org/simple" }
|
|
394
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
394
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
|
|
395
395
|
wheels = [
|
|
396
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
396
|
+
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
|
397
397
|
]
|
|
398
398
|
|
|
399
399
|
[[package]]
|
|
@@ -7,8 +7,8 @@ Plexus — thin Python SDK for sending telemetry to the Plexus gateway.
|
|
|
7
7
|
px.send("temperature", 72.5)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from plexus.client import Plexus, PlexusError,
|
|
10
|
+
from plexus.client import AuthenticationError, Plexus, PlexusError, read_mjpeg_frames
|
|
11
11
|
from plexus.config import RetryConfig
|
|
12
12
|
|
|
13
|
-
__version__ = "0.
|
|
14
|
-
__all__ = ["
|
|
13
|
+
__version__ = "0.9.1"
|
|
14
|
+
__all__ = ["AuthenticationError", "Plexus", "PlexusError", "RetryConfig", "read_mjpeg_frames"]
|