plexus-python 0.4.5__tar.gz → 0.4.7__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.4.5 → plexus_python-0.4.7}/API.md +41 -5
- {plexus_python-0.4.5 → plexus_python-0.4.7}/CHANGELOG.md +22 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/PKG-INFO +19 -1
- {plexus_python-0.4.5 → plexus_python-0.4.7}/README.md +18 -0
- plexus_python-0.4.7/examples/.python-version +1 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/examples/README.md +1 -0
- plexus_python-0.4.7/examples/mac_metrics.py +81 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/examples/mqtt.py +1 -1
- plexus_python-0.4.7/examples/pyproject.toml +15 -0
- plexus_python-0.4.7/examples/uv.lock +740 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/plexus/__init__.py +1 -1
- {plexus_python-0.4.5 → plexus_python-0.4.7}/plexus/client.py +99 -10
- {plexus_python-0.4.5 → plexus_python-0.4.7}/plexus/ws.py +18 -1
- {plexus_python-0.4.5 → plexus_python-0.4.7}/pyproject.toml +1 -1
- {plexus_python-0.4.5 → plexus_python-0.4.7}/tests/test_basic.py +18 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/tests/test_ws.py +46 -0
- plexus_python-0.4.7/uv.lock +1167 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/.gitignore +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/AGENTS.md +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/CONTRIBUTING.md +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/LICENSE +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/SECURITY.md +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/examples/basic.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/examples/can.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/examples/mavlink.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/plexus/buffer.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/plexus/cli.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/plexus/config.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/scripts/plexus.service +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/scripts/scan_buses.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/scripts/setup.sh +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/tests/test_buffer.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/tests/test_config.py +0 -0
- {plexus_python-0.4.5 → plexus_python-0.4.7}/tests/test_retry.py +0 -0
|
@@ -98,7 +98,7 @@ x-api-key: plx_xxxxx
|
|
|
98
98
|
| ------------ | ------ | -------- | ---------------------------------------------- |
|
|
99
99
|
| `metric` | string | Yes | Metric name (e.g., `temperature`, `motor.rpm`) |
|
|
100
100
|
| `value` | any | Yes | See supported value types below |
|
|
101
|
-
| `timestamp` | float | No | Unix timestamp (
|
|
101
|
+
| `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
102
|
| `source_id` | string | Yes | Your source identifier |
|
|
103
103
|
| `tags` | object | No | Key-value labels |
|
|
104
104
|
| `session_id` | string | No | Group data into sessions |
|
|
@@ -168,18 +168,22 @@ Devices authenticate using an API key. The `source_id` in the request is the dev
|
|
|
168
168
|
// Server → Device
|
|
169
169
|
{
|
|
170
170
|
"type": "authenticated",
|
|
171
|
-
"source_id": "drone-01"
|
|
171
|
+
"source_id": "drone-01",
|
|
172
|
+
"server_time_ms": 1746100800000
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
// Server → Device (collision case)
|
|
175
176
|
{
|
|
176
177
|
"type": "authenticated",
|
|
177
|
-
"source_id": "drone-01_2"
|
|
178
|
+
"source_id": "drone-01_2",
|
|
179
|
+
"server_time_ms": 1746100800000
|
|
178
180
|
}
|
|
179
181
|
```
|
|
180
182
|
|
|
181
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.
|
|
182
184
|
|
|
185
|
+
`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
|
+
|
|
183
187
|
`install_id` is a stable per-installation UUID, generated on the device's first run and saved to `~/.plexus/config.json`. It lets the server distinguish a rebooting device from a new device trying to claim an existing name. Legacy SDKs that omit `install_id` continue to work as before (the server passes the declared `source_id` through unchanged).
|
|
184
188
|
|
|
185
189
|
### Message Types (Dashboard → Device)
|
|
@@ -354,6 +358,9 @@ func main() {
|
|
|
354
358
|
#include <WiFi.h>
|
|
355
359
|
#include <HTTPClient.h>
|
|
356
360
|
|
|
361
|
+
// Call configTime(0, 0, "pool.ntp.org") in setup() before sending.
|
|
362
|
+
// time(nullptr) returns 0 until NTP sync completes — omit the timestamp
|
|
363
|
+
// field entirely if you cannot guarantee NTP sync at send time.
|
|
357
364
|
void sendToPlexus(const char* metric, float value) {
|
|
358
365
|
HTTPClient http;
|
|
359
366
|
http.begin("https://plexus-gateway.fly.dev/ingest");
|
|
@@ -363,7 +370,7 @@ void sendToPlexus(const char* metric, float value) {
|
|
|
363
370
|
String payload = "{\"points\":[{";
|
|
364
371
|
payload += "\"metric\":\"" + String(metric) + "\",";
|
|
365
372
|
payload += "\"value\":" + String(value) + ",";
|
|
366
|
-
payload += "\"timestamp\":" + String(
|
|
373
|
+
payload += "\"timestamp\":" + String(time(nullptr)) + ",";
|
|
367
374
|
payload += "\"source_id\":\"esp32-001\"";
|
|
368
375
|
payload += "}]}";
|
|
369
376
|
|
|
@@ -469,10 +476,39 @@ class MySensor(BaseSensor):
|
|
|
469
476
|
| 404 | Resource not found |
|
|
470
477
|
| 410 | Resource expired |
|
|
471
478
|
|
|
479
|
+
## Clock correction
|
|
480
|
+
|
|
481
|
+
Embedded devices commonly boot with a wrong system clock — no hardware RTC, NTP unreachable on first boot, or a fresh OS image whose filesystem timestamp is months in the past. Without correction, all telemetry lands at the wrong place on the timeline.
|
|
482
|
+
|
|
483
|
+
The Python SDK corrects for this automatically over WebSocket. On every connection the gateway returns `server_time_ms` in the `authenticated` frame. The SDK computes `offset = server_time - device_time` and adds it to every timestamp it generates. Data lands at the right time on the dashboard regardless of what the device clock says.
|
|
484
|
+
|
|
485
|
+
**When the correction applies:**
|
|
486
|
+
|
|
487
|
+
The offset is applied when `timestamp` is omitted (the SDK generates the time). If you pass an explicit `timestamp`, it is used as-is — the SDK cannot tell whether your value is a wall-clock time or a hardware-relative counter, so it leaves it alone.
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
px.send("temperature", 72.5) # SDK picks time → correction applied
|
|
491
|
+
px.send("temperature", 72.5, timestamp=t) # your timestamp → used as-is, no correction
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**When to pass an explicit timestamp:**
|
|
495
|
+
- You have a reliable wall-clock source (GPS, trusted hardware RTC, host NTP)
|
|
496
|
+
- You are replaying or backfilling historical data
|
|
497
|
+
- Your sensor provides its own wall-clock timestamp
|
|
498
|
+
|
|
499
|
+
**When to omit timestamp:**
|
|
500
|
+
- The device may have booted without NTP (Raspberry Pi, Jetson, field robots without network on first boot)
|
|
501
|
+
- You have no reliable external time source
|
|
502
|
+
|
|
503
|
+
**Known limitations:**
|
|
504
|
+
- 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 transport (`transport="http"`) does not receive clock sync — timestamps default to the device clock uncorrected.
|
|
506
|
+
- `send_batch()` takes one shared `timestamp` for the whole batch, not per-point. For per-point timestamps, call `send()` in a loop.
|
|
507
|
+
|
|
472
508
|
## Best Practices
|
|
473
509
|
|
|
474
510
|
- **Batch points** - Send up to 100 points per request for HTTP
|
|
475
|
-
- **
|
|
511
|
+
- **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
|
|
476
512
|
- **Consistent source_id** - Use the same ID for each physical device/source
|
|
477
513
|
- **Use tags** - Label data for filtering (e.g., `{"location": "lab"}`)
|
|
478
514
|
- **Use sessions** - Group related data for easier analysis
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.7] - 2026-05-14 - Video streaming API
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `Plexus.send_video_frame(frame, camera_id, quality, timestamp)` — high-level
|
|
8
|
+
API for streaming camera frames. Accepts a numpy array (e.g. from
|
|
9
|
+
`cv2.VideoCapture.read()`), handles JPEG encoding, base64, dimensions, and
|
|
10
|
+
auth wait internally. Requires `transport="ws"` and `opencv-python`.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Gateway WebSocket URL (`wss://plexus-gateway.fly.dev`) is now the SDK
|
|
15
|
+
default — no need to pass `ws_url` explicitly.
|
|
16
|
+
- Removed the `[plexus] endpoint: …` line from the connection printout.
|
|
17
|
+
|
|
18
|
+
### Performance
|
|
19
|
+
|
|
20
|
+
- Eliminated per-frame `buf.tobytes()` copy in `send_video_frame` by passing
|
|
21
|
+
the numpy buffer directly to `base64.b64encode` (buffer protocol).
|
|
22
|
+
- `base64` imported at module level; `cv2` imported once on first call and
|
|
23
|
+
cached, removing repeated import overhead from the hot path.
|
|
24
|
+
|
|
3
25
|
## [0.4.5] - 2026-04-27 - Stderr status output (re-release of 0.4.4)
|
|
4
26
|
|
|
5
27
|
Same code as 0.4.4 — the 0.4.4 publish workflow failed lint on a stray
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plexus-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
4
4
|
Summary: Thin Python SDK for Plexus — send telemetry in one line
|
|
5
5
|
Project-URL: Homepage, https://plexus.dev
|
|
6
6
|
Project-URL: Documentation, https://docs.plexus.dev
|
|
@@ -136,6 +136,24 @@ px.buffer_size()
|
|
|
136
136
|
px.flush_buffer()
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
## Timestamps and clock correction
|
|
140
|
+
|
|
141
|
+
By default — `px.send("temp", 72.5)` with no `timestamp` argument — the SDK picks the time itself. Over WebSocket, it synchronizes with the gateway clock on every connection, so data lands at the right place on the timeline even if the device's system clock is wrong (no NTP on first boot, stale RTC, fresh OS image).
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
px.send("temperature", 72.5) # SDK picks time; gateway-synced over WS
|
|
145
|
+
px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no correction
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Pass an explicit timestamp when** you have a reliable external time source (GPS, trusted RTC, host NTP) or are replaying historical data with known timestamps.
|
|
149
|
+
|
|
150
|
+
**Omit timestamp when** the device may have booted without NTP — which is the default on Raspberry Pi, Jetson, and most embedded Linux boards without a network connection at first boot.
|
|
151
|
+
|
|
152
|
+
**Known limits:**
|
|
153
|
+
- Clock sync refreshes on WebSocket (re)connect. A device with a drifting RTC that stays connected for many days accumulates uncorrected drift between reconnects.
|
|
154
|
+
- HTTP-only transport (`transport="http"`) does not receive clock sync — timestamps default to the uncorrected device clock.
|
|
155
|
+
- `send_batch()` shares one timestamp across the whole batch. For per-point timestamps, call `send()` in a loop.
|
|
156
|
+
|
|
139
157
|
## Transport
|
|
140
158
|
|
|
141
159
|
By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — same wire protocol as the C SDK. This gives you:
|
|
@@ -102,6 +102,24 @@ px.buffer_size()
|
|
|
102
102
|
px.flush_buffer()
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
## Timestamps and clock correction
|
|
106
|
+
|
|
107
|
+
By default — `px.send("temp", 72.5)` with no `timestamp` argument — the SDK picks the time itself. Over WebSocket, it synchronizes with the gateway clock on every connection, so data lands at the right place on the timeline even if the device's system clock is wrong (no NTP on first boot, stale RTC, fresh OS image).
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
px.send("temperature", 72.5) # SDK picks time; gateway-synced over WS
|
|
111
|
+
px.send("temperature", 72.5, timestamp=t) # your timestamp, used as-is, no correction
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Pass an explicit timestamp when** you have a reliable external time source (GPS, trusted RTC, host NTP) or are replaying historical data with known timestamps.
|
|
115
|
+
|
|
116
|
+
**Omit timestamp when** the device may have booted without NTP — which is the default on Raspberry Pi, Jetson, and most embedded Linux boards without a network connection at first boot.
|
|
117
|
+
|
|
118
|
+
**Known limits:**
|
|
119
|
+
- Clock sync refreshes on WebSocket (re)connect. A device with a drifting RTC that stays connected for many days accumulates uncorrected drift between reconnects.
|
|
120
|
+
- HTTP-only transport (`transport="http"`) does not receive clock sync — timestamps default to the uncorrected device clock.
|
|
121
|
+
- `send_batch()` shares one timestamp across the whole batch. For per-point timestamps, call `send()` in a loop.
|
|
122
|
+
|
|
105
123
|
## Transport
|
|
106
124
|
|
|
107
125
|
By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — same wire protocol as the C SDK. This gives you:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -9,5 +9,6 @@ Each script is standalone — copy into your project, adjust the `source_id`, an
|
|
|
9
9
|
| `can.py` | Vehicle CAN bus (with optional DBC decode) | `python-can`, `cantools` |
|
|
10
10
|
| `mqtt.py` | MQTT broker → Plexus bridge | `paho-mqtt` |
|
|
11
11
|
| `i2c_bme280.py` | Raspberry Pi environmental sensor | `adafruit-circuitpython-bme280` |
|
|
12
|
+
| `mac_metrics.py` | Mac system metrics + spike/pressure events | `psutil` |
|
|
12
13
|
|
|
13
14
|
The pattern is always the same: use whatever library you'd use anyway, then call `px.send(metric, value)`. Plexus stays out of your decode path.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mac system metrics with structured event logs.
|
|
3
|
+
|
|
4
|
+
Install deps:
|
|
5
|
+
pip install plexus-python psutil
|
|
6
|
+
|
|
7
|
+
Run:
|
|
8
|
+
python examples/mac_metrics.py --api-key plx_xxx
|
|
9
|
+
python examples/mac_metrics.py --api-key plx_xxx --interval 10
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import time
|
|
14
|
+
import psutil
|
|
15
|
+
from plexus import Plexus
|
|
16
|
+
|
|
17
|
+
parser = argparse.ArgumentParser(description="Stream Mac system metrics to Plexus.")
|
|
18
|
+
parser.add_argument("--api-key", required=True, help="Plexus API key (plx_xxx)")
|
|
19
|
+
parser.add_argument("--interval", type=float, default=5.0, metavar="SECONDS",
|
|
20
|
+
help="Sampling interval in seconds (default: 5)")
|
|
21
|
+
parser.add_argument("--source-id", default="macbook", help="Device source ID (default: macbook)")
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
px = Plexus(api_key=args.api_key, source_id=args.source_id)
|
|
25
|
+
|
|
26
|
+
CPU_SPIKE_THRESHOLD = 80.0 # %
|
|
27
|
+
MEM_PRESSURE_THRESHOLD = 85.0 # %
|
|
28
|
+
|
|
29
|
+
prev_net = psutil.net_io_counters()
|
|
30
|
+
prev_disk = psutil.disk_io_counters()
|
|
31
|
+
prev_charging = None
|
|
32
|
+
|
|
33
|
+
while True:
|
|
34
|
+
net = psutil.net_io_counters()
|
|
35
|
+
disk = psutil.disk_io_counters()
|
|
36
|
+
|
|
37
|
+
cpu = psutil.cpu_percent(interval=None)
|
|
38
|
+
mem = psutil.virtual_memory()
|
|
39
|
+
|
|
40
|
+
# Numeric metrics
|
|
41
|
+
px.send("cpu.percent", cpu)
|
|
42
|
+
px.send("memory.used_gb", mem.used / 1e9)
|
|
43
|
+
px.send("memory.percent", mem.percent)
|
|
44
|
+
px.send("net.rx_mbps", (net.bytes_recv - prev_net.bytes_recv) / 1e6)
|
|
45
|
+
px.send("net.tx_mbps", (net.bytes_sent - prev_net.bytes_sent) / 1e6)
|
|
46
|
+
px.send("disk.read_mbps", (disk.read_bytes - prev_disk.read_bytes) / 1e6)
|
|
47
|
+
px.send("disk.write_mbps", (disk.write_bytes - prev_disk.write_bytes) / 1e6)
|
|
48
|
+
px.send("disk.free_gb", psutil.disk_usage("/").free / 1e9)
|
|
49
|
+
|
|
50
|
+
# Battery metrics + charge-state change event
|
|
51
|
+
battery = psutil.sensors_battery()
|
|
52
|
+
if battery:
|
|
53
|
+
px.send("battery.percent", battery.percent)
|
|
54
|
+
charging = battery.power_plugged
|
|
55
|
+
if charging != prev_charging and prev_charging is not None:
|
|
56
|
+
px.event("battery.state_change", {
|
|
57
|
+
"charging": charging,
|
|
58
|
+
"percent": battery.percent,
|
|
59
|
+
})
|
|
60
|
+
prev_charging = charging
|
|
61
|
+
|
|
62
|
+
# CPU spike event — fires once per interval when threshold is crossed
|
|
63
|
+
if cpu >= CPU_SPIKE_THRESHOLD:
|
|
64
|
+
top = max(psutil.process_iter(["name", "cpu_percent"]), key=lambda p: p.info["cpu_percent"] or 0)
|
|
65
|
+
px.event("cpu.spike", {
|
|
66
|
+
"cpu_percent": cpu,
|
|
67
|
+
"top_process": top.info["name"],
|
|
68
|
+
"top_process_percent": top.info["cpu_percent"],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
# Memory pressure event
|
|
72
|
+
if mem.percent >= MEM_PRESSURE_THRESHOLD:
|
|
73
|
+
top = max(psutil.process_iter(["name", "memory_percent"]), key=lambda p: p.info["memory_percent"] or 0)
|
|
74
|
+
px.event("memory.pressure", {
|
|
75
|
+
"memory_percent": mem.percent,
|
|
76
|
+
"top_process": top.info["name"],
|
|
77
|
+
"top_process_percent": round(top.info["memory_percent"], 1),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
prev_net, prev_disk = net, disk
|
|
81
|
+
time.sleep(args.interval)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "examples"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Standalone example scripts for plexus-python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"adafruit-circuitpython-bme280>=2.6.32",
|
|
9
|
+
"cantools>=41.3.1",
|
|
10
|
+
"paho-mqtt>=2.1.0",
|
|
11
|
+
"plexus-python>=0.4.6",
|
|
12
|
+
"psutil>=7.2.2",
|
|
13
|
+
"pymavlink>=2.4.49",
|
|
14
|
+
"python-can>=4.6.1",
|
|
15
|
+
]
|