plexus-python 0.7.1__tar.gz → 0.9.0__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.7.1 → plexus_python-0.9.0}/AGENTS.md +15 -10
- {plexus_python-0.7.1 → plexus_python-0.9.0}/API.md +56 -163
- {plexus_python-0.7.1 → plexus_python-0.9.0}/CHANGELOG.md +55 -2
- {plexus_python-0.7.1 → plexus_python-0.9.0}/PKG-INFO +15 -26
- {plexus_python-0.7.1 → plexus_python-0.9.0}/README.md +9 -20
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/mac_metrics.py +2 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/thermal_camera.py +1 -2
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/uv.lock +4 -4
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/__init__.py +3 -3
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/buffer.py +17 -16
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/cameras/thermal.py +6 -6
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/cli.py +7 -9
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/client.py +210 -143
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/config.py +5 -6
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/ws.py +97 -37
- {plexus_python-0.7.1 → plexus_python-0.9.0}/pyproject.toml +5 -5
- {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/scan_buses.py +1 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_basic.py +1 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_buffer.py +0 -2
- {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_config.py +1 -2
- {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_retry.py +102 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_video.py +0 -1
- {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_ws.py +137 -8
- {plexus_python-0.7.1 → plexus_python-0.9.0}/uv.lock +92 -96
- plexus_python-0.7.1/skills/plexus/SKILL.md +0 -189
- plexus_python-0.7.1/skills/plexus/references/api.md +0 -331
- plexus_python-0.7.1/skills/plexus/references/sdk.md +0 -227
- {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/.gitignore +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/CONTRIBUTING.md +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/LICENSE +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/SECURITY.md +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/TODO.md +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/.python-version +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/README.md +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/basic.py +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/can.py +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/mavlink.py +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/mqtt.py +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/examples/pyproject.toml +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/_log.py +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/plexus/cameras/__init__.py +2 -2
- {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/plexus.service +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/release.sh +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/scripts/setup.sh +0 -0
- {plexus_python-0.7.1 → plexus_python-0.9.0}/tests/test_thermal.py +0 -0
|
@@ -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
|
|
|
@@ -30,7 +30,7 @@ Then control streaming, recording, and configuration from [app.plexus.company/de
|
|
|
30
30
|
Send data directly via HTTP:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
curl -X POST https://
|
|
33
|
+
curl -X POST https://gateway.plexus.company/ingest \
|
|
34
34
|
-H "x-api-key: YOUR_API_KEY" \
|
|
35
35
|
-H "Content-Type: application/json" \
|
|
36
36
|
-d '{
|
|
@@ -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,21 +87,19 @@ 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
|
}
|
|
95
94
|
```
|
|
96
95
|
|
|
97
|
-
| Field | Type | Required | Description
|
|
98
|
-
| ------------ | ------ | -------- |
|
|
99
|
-
| `metric` | string | Yes | Metric name (e.g., `temperature`, `motor.rpm`)
|
|
100
|
-
| `value` | any | Yes | See supported value types below
|
|
96
|
+
| Field | Type | Required | Description |
|
|
97
|
+
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `metric` | string | Yes | Metric name (e.g., `temperature`, `motor.rpm`) |
|
|
99
|
+
| `value` | any | Yes | See supported value types below |
|
|
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
|
-
| `source_id` | string | Yes | Your source identifier
|
|
103
|
-
| `tags` | object | No | Key-value labels
|
|
104
|
-
| `session_id` | string | No | Group data into sessions |
|
|
101
|
+
| `source_id` | string | Yes | Your source identifier |
|
|
102
|
+
| `tags` | object | No | Key-value labels |
|
|
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
|
|
|
@@ -286,7 +208,7 @@ import requests
|
|
|
286
208
|
import time
|
|
287
209
|
|
|
288
210
|
requests.post(
|
|
289
|
-
"https://
|
|
211
|
+
"https://gateway.plexus.company/ingest",
|
|
290
212
|
headers={"x-api-key": "plx_xxxxx"},
|
|
291
213
|
json={
|
|
292
214
|
"points": [{
|
|
@@ -302,7 +224,7 @@ requests.post(
|
|
|
302
224
|
### JavaScript
|
|
303
225
|
|
|
304
226
|
```javascript
|
|
305
|
-
await fetch("https://
|
|
227
|
+
await fetch("https://gateway.plexus.company/ingest", {
|
|
306
228
|
method: "POST",
|
|
307
229
|
headers: {
|
|
308
230
|
"x-api-key": "plx_xxxxx",
|
|
@@ -344,7 +266,7 @@ func main() {
|
|
|
344
266
|
}
|
|
345
267
|
|
|
346
268
|
body, _ := json.Marshal(points)
|
|
347
|
-
req, _ := http.NewRequest("POST", "https://
|
|
269
|
+
req, _ := http.NewRequest("POST", "https://gateway.plexus.company/ingest", bytes.NewBuffer(body))
|
|
348
270
|
req.Header.Set("x-api-key", "plx_xxxxx")
|
|
349
271
|
req.Header.Set("Content-Type", "application/json")
|
|
350
272
|
|
|
@@ -363,7 +285,7 @@ func main() {
|
|
|
363
285
|
// field entirely if you cannot guarantee NTP sync at send time.
|
|
364
286
|
void sendToPlexus(const char* metric, float value) {
|
|
365
287
|
HTTPClient http;
|
|
366
|
-
http.begin("https://
|
|
288
|
+
http.begin("https://gateway.plexus.company/ingest");
|
|
367
289
|
http.addHeader("Content-Type", "application/json");
|
|
368
290
|
http.addHeader("x-api-key", "plx_xxxxx");
|
|
369
291
|
|
|
@@ -386,7 +308,7 @@ void sendToPlexus(const char* metric, float value) {
|
|
|
386
308
|
API_KEY="plx_xxxxx"
|
|
387
309
|
SOURCE_ID="sensor-001"
|
|
388
310
|
|
|
389
|
-
curl -X POST https://
|
|
311
|
+
curl -X POST https://gateway.plexus.company/ingest \
|
|
390
312
|
-H "x-api-key: $API_KEY" \
|
|
391
313
|
-H "Content-Type: application/json" \
|
|
392
314
|
-d "{
|
|
@@ -430,40 +352,9 @@ while True:
|
|
|
430
352
|
px.send("attitude.pitch", msg.pitch)
|
|
431
353
|
```
|
|
432
354
|
|
|
433
|
-
See [docs.plexus.dev/recipes](https://docs.plexus.dev/recipes) for more.
|
|
434
|
-
|
|
435
355
|
## Python SDK with Sensor Drivers
|
|
436
356
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
```bash
|
|
440
|
-
pip install plexus-python[sensors]
|
|
441
|
-
plexus start
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### Supported Sensors
|
|
445
|
-
|
|
446
|
-
| Sensor | Type | Metrics | I2C Address |
|
|
447
|
-
| ------- | ----------- | ------------------------------------------------------------- | ----------- |
|
|
448
|
-
| MPU6050 | 6-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68, 0x69 |
|
|
449
|
-
| MPU9250 | 9-axis IMU | `accel_x`, `accel_y`, `accel_z`, `gyro_x`, `gyro_y`, `gyro_z` | 0x68 |
|
|
450
|
-
| BME280 | Environment | `temperature`, `humidity`, `pressure` | 0x76, 0x77 |
|
|
451
|
-
|
|
452
|
-
### Custom Sensors
|
|
453
|
-
|
|
454
|
-
```python
|
|
455
|
-
from plexus.sensors import BaseSensor, SensorReading
|
|
456
|
-
|
|
457
|
-
class MySensor(BaseSensor):
|
|
458
|
-
name = "MySensor"
|
|
459
|
-
metrics = ["voltage", "current"]
|
|
460
|
-
|
|
461
|
-
def read(self):
|
|
462
|
-
return [
|
|
463
|
-
SensorReading("voltage", read_adc(0) * 3.3),
|
|
464
|
-
SensorReading("current", read_adc(1) * 0.1),
|
|
465
|
-
]
|
|
466
|
-
```
|
|
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).
|
|
467
358
|
|
|
468
359
|
## Errors
|
|
469
360
|
|
|
@@ -492,24 +383,26 @@ px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is
|
|
|
492
383
|
```
|
|
493
384
|
|
|
494
385
|
**When to pass an explicit timestamp:**
|
|
386
|
+
|
|
495
387
|
- You have a reliable wall-clock source (GPS, trusted hardware RTC, host NTP)
|
|
496
388
|
- You are replaying or backfilling historical data
|
|
497
389
|
- Your sensor provides its own wall-clock timestamp
|
|
498
390
|
|
|
499
391
|
**When to omit timestamp:**
|
|
392
|
+
|
|
500
393
|
- The device may have booted without NTP (Raspberry Pi, Jetson, field robots without network on first boot)
|
|
501
394
|
- You have no reliable external time source
|
|
502
395
|
|
|
503
396
|
**Known limitations:**
|
|
397
|
+
|
|
504
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.
|
|
505
|
-
- HTTP
|
|
506
|
-
- `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.
|
|
507
401
|
|
|
508
402
|
## Best Practices
|
|
509
403
|
|
|
510
404
|
- **Batch points** - Send up to 100 points per request for HTTP
|
|
511
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
|
|
512
406
|
- **Consistent source_id** - Use the same ID for each physical device/source
|
|
513
|
-
- **Use tags** - Label data for filtering (e.g., `{"location": "lab"}`)
|
|
514
|
-
- **
|
|
515
|
-
- **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.0] - 2026-08-27 - Flexible values and the real slug rule
|
|
6
|
+
|
|
7
|
+
Two fixes for data that looked like it was sent but never arrived, plus the
|
|
8
|
+
removal of a dead API.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`send()` now infers a point's class from its value type** instead of always
|
|
13
|
+
tagging `class="metric"`. Numbers stay metrics; strings, bools, dicts and
|
|
14
|
+
lists become events. The gateway rejects a non-numeric value on
|
|
15
|
+
`class="metric"` and drops the *whole frame* — so `px.send("state", "RUNNING")`
|
|
16
|
+
silently took every other point in that batch down with it. Non-numeric values
|
|
17
|
+
have been advertised since 0.2.0; this is what makes them work.
|
|
18
|
+
An explicit `data_class="metric"` with a non-numeric value now raises
|
|
19
|
+
`PlexusError` locally rather than failing on the wire.
|
|
20
|
+
|
|
21
|
+
- **`source_id` validation matched an older, stricter rule than the server.**
|
|
22
|
+
The SDK enforced `^[a-z0-9][a-z0-9_-]{1,62}$` — no dots, minimum two
|
|
23
|
+
characters — while the gateway accepts `^[a-z0-9][a-z0-9._-]*$` up to 256
|
|
24
|
+
characters. Names production would have accepted (`rack.01`, `a`) raised
|
|
25
|
+
`ValueError` in the constructor before a single byte left the host, which
|
|
26
|
+
reads as an auth problem and is not one. The SDK now mirrors the wire rule.
|
|
27
|
+
Uuid-shaped ids are still rejected: the app resolves those as internal ids,
|
|
28
|
+
so such a source would be unreachable.
|
|
29
|
+
|
|
30
|
+
- Buffer drains are chunked, so a backlog over the gateway's 10k-points-per-batch
|
|
31
|
+
ceiling no longer wedges every subsequent send. The WebSocket auth wait is paid
|
|
32
|
+
once, on the first send, instead of stalling each call.
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
|
|
36
|
+
- `run()` context manager and its `POST /api/runs` plumbing. The `/api/runs` route never
|
|
37
|
+
existed on the platform (both notifications failed silently on every call), the ingest
|
|
38
|
+
loader drops `run_id` from points, and nothing was ever stored — so removal loses no
|
|
39
|
+
data. To group a slice of data, use `tags` on each point.
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
|
|
43
|
+
- Bumped pillow to 12.3.0 and idna to 3.19 for published advisories.
|
|
44
|
+
|
|
45
|
+
## [0.8.0] - 2026-07-02 - Command concurrency control
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `concurrency` option on `on_command()` / `register_command()`. Defaults to `"accept"`
|
|
50
|
+
(unchanged behavior — overlapping invocations run concurrently). Set `"reject"` to refuse a
|
|
51
|
+
new invocation with an error result (`command already in progress: <name>`) while a previous
|
|
52
|
+
invocation of the same command is still running. Use it for handlers that drive exclusive
|
|
53
|
+
hardware (e.g. a pump init) so a client retry or double-click can't start two at once.
|
|
54
|
+
Protection is per command name; the immediate ack is still sent before the handler runs.
|
|
55
|
+
|
|
3
56
|
## [0.7.1] - 2026-06-02 - Remove install_id / source_id auto-suffix
|
|
4
57
|
|
|
5
58
|
### Changed
|
|
@@ -184,7 +237,7 @@
|
|
|
184
237
|
|
|
185
238
|
### Changed
|
|
186
239
|
|
|
187
|
-
- Gateway WebSocket URL (`wss://
|
|
240
|
+
- Gateway WebSocket URL (`wss://gateway.plexus.company`) is now the SDK
|
|
188
241
|
default — no need to pass `ws_url` explicitly.
|
|
189
242
|
- Removed the `[plexus] endpoint: …` line from the connection printout.
|
|
190
243
|
|
|
@@ -342,7 +395,7 @@ Breaking. `plexus-python` is now just the thin client — no agent, adapters, se
|
|
|
342
395
|
|
|
343
396
|
### Changed
|
|
344
397
|
|
|
345
|
-
- Default ingest endpoint points directly at the Plexus gateway (`https://
|
|
398
|
+
- Default ingest endpoint points directly at the Plexus gateway (`https://gateway.plexus.company/ingest`), not the Next.js app proxy
|
|
346
399
|
- Client raises `ValueError` clearly when no API key is available, instead of invoking a login flow
|
|
347
400
|
|
|
348
401
|
## [0.1.0] - Initial release
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: plexus-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Thin Python SDK for Plexus — send telemetry in one line
|
|
5
|
-
Project-URL: Homepage, https://plexus.
|
|
6
|
-
Project-URL: Documentation, https://docs.plexus.
|
|
5
|
+
Project-URL: Homepage, https://plexus.company
|
|
6
|
+
Project-URL: Documentation, https://docs.plexus.company
|
|
7
7
|
Project-URL: Repository, https://github.com/plexus-oss/plexus-python
|
|
8
8
|
Project-URL: Issues, https://github.com/plexus-oss/plexus-python/issues
|
|
9
|
-
Author-email: Plexus <
|
|
9
|
+
Author-email: Plexus <info@plexus.company>
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: fleet,hardware,iot,monitoring,observability,telemetry
|
|
@@ -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:
|
|
@@ -270,8 +259,8 @@ The SDK sends an `ack` frame before invoking the handler, then a `result` frame
|
|
|
270
259
|
| Variable | Description | Default |
|
|
271
260
|
| ----------------------- | ---------------------------- | -------------------------------- |
|
|
272
261
|
| `PLEXUS_API_KEY` | API key (required) | none |
|
|
273
|
-
| `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://
|
|
274
|
-
| `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://
|
|
262
|
+
| `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://gateway.plexus.company` |
|
|
263
|
+
| `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://gateway.plexus.company` |
|
|
275
264
|
|
|
276
265
|
## Architecture
|
|
277
266
|
|