plexus-python 0.5.1__tar.gz → 0.5.2__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.5.1 → plexus_python-0.5.2}/CHANGELOG.md +28 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/PKG-INFO +1 -1
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/uv.lock +3 -3
- {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/__init__.py +1 -1
- plexus_python-0.5.2/plexus/_log.py +15 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/client.py +53 -25
- {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/ws.py +2 -19
- {plexus_python-0.5.1 → plexus_python-0.5.2}/pyproject.toml +1 -1
- plexus_python-0.5.2/skills/plexus/SKILL.md +189 -0
- plexus_python-0.5.2/skills/plexus/references/api.md +331 -0
- plexus_python-0.5.2/skills/plexus/references/sdk.md +227 -0
- plexus_python-0.5.2/tests/test_basic.py +114 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_retry.py +2 -0
- plexus_python-0.5.1/tests/test_basic.py +0 -61
- {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/workflows/ci.yml +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/workflows/publish.yml +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/.gitignore +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/AGENTS.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/API.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/CODE_OF_CONDUCT.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/CONTRIBUTING.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/LICENSE +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/README.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/SECURITY.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/.python-version +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/README.md +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/basic.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/can.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/i2c_bme280.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/mac_metrics.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/mavlink.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/mqtt.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/pyproject.toml +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/buffer.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/cli.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/config.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/plexus.service +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/release.sh +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/scan_buses.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/setup.sh +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_buffer.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_config.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_video.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_ws.py +0 -0
- {plexus_python-0.5.1 → plexus_python-0.5.2}/uv.lock +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.2] - 2026-05-21 - DX hardening for hardware engineers
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Error messages now reference `plexus init` instead of the non-existent `plexus start`.
|
|
8
|
+
- `close()` now attempts to flush any buffered points before tearing down the transport,
|
|
9
|
+
preventing silent data loss on graceful shutdown.
|
|
10
|
+
- `persistent_buffer` default changed from `False` to `True` — store-and-forward is now
|
|
11
|
+
on by default, matching the `~/.plexus/config.json` default and the right choice for
|
|
12
|
+
field hardware. Pass `persistent_buffer=False` to opt out (e.g. in test fixtures).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `send_batch()` now accepts 3-tuples `(metric, value, timestamp)` alongside the existing
|
|
17
|
+
2-tuple form. Per-point timestamps let sensors on different interrupt timers share a
|
|
18
|
+
single batch call. 2-tuples continue to use the shared `timestamp` argument.
|
|
19
|
+
- `on_command()` now warns immediately via stderr if called after the WebSocket has already
|
|
20
|
+
authenticated, making the "register before first send()" ordering requirement visible
|
|
21
|
+
rather than silently broken.
|
|
22
|
+
- `source_id` is validated against `^[a-z0-9][a-z0-9_-]{1,62}$` at construction time.
|
|
23
|
+
Invalid names now raise `ValueError` with a clear message instead of failing obscurely
|
|
24
|
+
at the gateway.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `_say()` / `_QUIET` consolidated into a new internal `plexus/_log.py` module.
|
|
29
|
+
Previously duplicated verbatim between `client.py` and `ws.py`.
|
|
30
|
+
|
|
3
31
|
## [0.5.1] - 2026-05-19 - Binary video frames + non-blocking send
|
|
4
32
|
|
|
5
33
|
### Performance
|
|
@@ -674,11 +674,11 @@ wheels = [
|
|
|
674
674
|
|
|
675
675
|
[[package]]
|
|
676
676
|
name = "urllib3"
|
|
677
|
-
version = "2.
|
|
677
|
+
version = "2.7.0"
|
|
678
678
|
source = { registry = "https://pypi.org/simple" }
|
|
679
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
679
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
|
680
680
|
wheels = [
|
|
681
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
681
|
+
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
|
682
682
|
]
|
|
683
683
|
|
|
684
684
|
[[package]]
|
|
@@ -10,5 +10,5 @@ Plexus — thin Python SDK for sending telemetry to the Plexus gateway.
|
|
|
10
10
|
from plexus.client import Plexus, read_mjpeg_frames
|
|
11
11
|
from plexus.ws import WebSocketTransport
|
|
12
12
|
|
|
13
|
-
__version__ = "0.5.
|
|
13
|
+
__version__ = "0.5.2"
|
|
14
14
|
__all__ = ["Plexus", "WebSocketTransport", "read_mjpeg_frames"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
_QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _say(line: str) -> None:
|
|
8
|
+
"""Single-line status message to stderr. Skipped if PLEXUS_QUIET=1."""
|
|
9
|
+
if _QUIET:
|
|
10
|
+
return
|
|
11
|
+
try:
|
|
12
|
+
sys.stderr.write(f"[plexus] {line}\n")
|
|
13
|
+
sys.stderr.flush()
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
@@ -30,17 +30,16 @@ Usage:
|
|
|
30
30
|
px.send("temperature", read_temp())
|
|
31
31
|
time.sleep(0.01)
|
|
32
32
|
|
|
33
|
-
Note: Requires authentication. Run 'plexus
|
|
33
|
+
Note: Requires authentication. Run 'plexus init' or set PLEXUS_API_KEY.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
import gzip
|
|
37
37
|
import json
|
|
38
38
|
import logging
|
|
39
|
-
import
|
|
39
|
+
import re
|
|
40
40
|
import shutil
|
|
41
41
|
import socket
|
|
42
42
|
import subprocess
|
|
43
|
-
import sys
|
|
44
43
|
import threading
|
|
45
44
|
import time
|
|
46
45
|
import urllib.error
|
|
@@ -48,6 +47,7 @@ import urllib.request
|
|
|
48
47
|
from contextlib import contextmanager
|
|
49
48
|
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
|
50
49
|
|
|
50
|
+
from plexus._log import _say
|
|
51
51
|
from plexus.buffer import BufferBackend, MemoryBuffer, SqliteBuffer
|
|
52
52
|
from plexus.config import (
|
|
53
53
|
RetryConfig,
|
|
@@ -59,6 +59,7 @@ from plexus.config import (
|
|
|
59
59
|
get_source_id,
|
|
60
60
|
set_source_id,
|
|
61
61
|
)
|
|
62
|
+
|
|
62
63
|
logger = logging.getLogger(__name__)
|
|
63
64
|
|
|
64
65
|
|
|
@@ -101,20 +102,6 @@ class _ConnError(OSError):
|
|
|
101
102
|
pass
|
|
102
103
|
|
|
103
104
|
|
|
104
|
-
# Status messages to stderr so users running `python my_script.py` see what's
|
|
105
|
-
# happening without having to configure logging. Set PLEXUS_QUIET=1 to disable.
|
|
106
|
-
_QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _say(line: str) -> None:
|
|
110
|
-
if _QUIET:
|
|
111
|
-
return
|
|
112
|
-
try:
|
|
113
|
-
sys.stderr.write(f"[plexus] {line}\n")
|
|
114
|
-
sys.stderr.flush()
|
|
115
|
-
except Exception:
|
|
116
|
-
pass
|
|
117
|
-
|
|
118
105
|
# Flexible value type - supports any JSON-serializable value
|
|
119
106
|
FlexValue = Union[int, float, str, bool, Dict[str, Any], List[Any]]
|
|
120
107
|
|
|
@@ -161,6 +148,18 @@ class AuthenticationError(PlexusError):
|
|
|
161
148
|
pass
|
|
162
149
|
|
|
163
150
|
|
|
151
|
+
_SOURCE_ID_RE = re.compile(r'^[a-z0-9][a-z0-9_-]{1,62}$')
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _validate_source_id(source_id: str) -> None:
|
|
155
|
+
if not _SOURCE_ID_RE.match(source_id):
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Invalid source_id {source_id!r}. "
|
|
158
|
+
"Must match ^[a-z0-9][a-z0-9_-]{1,62}$ "
|
|
159
|
+
"(lowercase letters, digits, hyphens, underscores; start with letter or digit)."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
164
163
|
class Plexus:
|
|
165
164
|
"""
|
|
166
165
|
Client for sending sensor data to Plexus.
|
|
@@ -186,7 +185,7 @@ class Plexus:
|
|
|
186
185
|
timeout: float = 10.0,
|
|
187
186
|
retry_config: Optional[RetryConfig] = None,
|
|
188
187
|
max_buffer_size: int = 10000,
|
|
189
|
-
persistent_buffer: bool =
|
|
188
|
+
persistent_buffer: bool = True,
|
|
190
189
|
buffer_path: Optional[str] = None,
|
|
191
190
|
transport: str = "ws",
|
|
192
191
|
ws_url: Optional[str] = None,
|
|
@@ -201,6 +200,7 @@ class Plexus:
|
|
|
201
200
|
self.endpoint = (endpoint or get_endpoint()).rstrip("/")
|
|
202
201
|
self.gateway_url = get_gateway_url()
|
|
203
202
|
self.source_id = source_id or get_source_id()
|
|
203
|
+
_validate_source_id(self.source_id)
|
|
204
204
|
self.timeout = timeout
|
|
205
205
|
self.retry_config = retry_config or RetryConfig()
|
|
206
206
|
self._max_buffer_size = max_buffer_size
|
|
@@ -360,7 +360,7 @@ class Plexus:
|
|
|
360
360
|
|
|
361
361
|
def send_batch(
|
|
362
362
|
self,
|
|
363
|
-
points: List[Tuple[str, FlexValue]],
|
|
363
|
+
points: List[Union[Tuple[str, FlexValue], Tuple[str, FlexValue, float]]],
|
|
364
364
|
timestamp: Optional[float] = None,
|
|
365
365
|
tags: Optional[Dict[str, str]] = None,
|
|
366
366
|
) -> bool:
|
|
@@ -368,8 +368,11 @@ class Plexus:
|
|
|
368
368
|
Send multiple metrics at once.
|
|
369
369
|
|
|
370
370
|
Args:
|
|
371
|
-
points: List of (metric, value)
|
|
372
|
-
|
|
371
|
+
points: List of (metric, value) or (metric, value, timestamp) tuples.
|
|
372
|
+
Values can be any FlexValue type. Per-point timestamps override
|
|
373
|
+
the shared timestamp argument.
|
|
374
|
+
timestamp: Shared timestamp for points that don't supply their own.
|
|
375
|
+
If not provided, uses current time.
|
|
373
376
|
tags: Shared tags for all points
|
|
374
377
|
|
|
375
378
|
Returns:
|
|
@@ -382,9 +385,23 @@ class Plexus:
|
|
|
382
385
|
("robot.state", "RUNNING"),
|
|
383
386
|
("position", {"x": 1.0, "y": 2.0}),
|
|
384
387
|
])
|
|
388
|
+
|
|
389
|
+
# Per-point timestamps (e.g. sensors on different interrupt timers):
|
|
390
|
+
px.send_batch([
|
|
391
|
+
("imu.accel_x", 0.12, t_imu),
|
|
392
|
+
("pressure", 1013.2, t_baro),
|
|
393
|
+
("temperature", 22.4), # uses shared timestamp
|
|
394
|
+
])
|
|
385
395
|
"""
|
|
386
|
-
|
|
387
|
-
data_points = [
|
|
396
|
+
default_ts_ms = self._normalize_ts_ms(timestamp)
|
|
397
|
+
data_points = []
|
|
398
|
+
for p in points:
|
|
399
|
+
if len(p) == 3:
|
|
400
|
+
m, v, t = p
|
|
401
|
+
data_points.append(self._make_point(m, v, self._normalize_ts_ms(t), tags))
|
|
402
|
+
else:
|
|
403
|
+
m, v = p
|
|
404
|
+
data_points.append(self._make_point(m, v, default_ts_ms, tags))
|
|
388
405
|
return self._send_points(data_points)
|
|
389
406
|
|
|
390
407
|
def _ensure_ws(self):
|
|
@@ -656,6 +673,12 @@ class Plexus:
|
|
|
656
673
|
if self.transport != "ws":
|
|
657
674
|
raise PlexusError("on_command requires transport='ws'")
|
|
658
675
|
ws = self._ensure_ws()
|
|
676
|
+
if ws.is_authenticated:
|
|
677
|
+
_say(
|
|
678
|
+
f"⚠ on_command('{name}') called after connection is already authenticated — "
|
|
679
|
+
"command will not be advertised to the dashboard until next reconnect. "
|
|
680
|
+
"Call on_command() before the first send()."
|
|
681
|
+
)
|
|
659
682
|
ws.register_command(name, handler, description=description, params=params)
|
|
660
683
|
|
|
661
684
|
def _send_points(self, points: List[Dict[str, Any]]) -> bool:
|
|
@@ -673,7 +696,7 @@ class Plexus:
|
|
|
673
696
|
"""
|
|
674
697
|
if not self.api_key:
|
|
675
698
|
raise AuthenticationError(
|
|
676
|
-
"No API key configured. Run 'plexus
|
|
699
|
+
"No API key configured. Run 'plexus init' or set PLEXUS_API_KEY"
|
|
677
700
|
)
|
|
678
701
|
|
|
679
702
|
# Include any previously buffered points
|
|
@@ -905,7 +928,12 @@ class Plexus:
|
|
|
905
928
|
self._store_frames = False
|
|
906
929
|
|
|
907
930
|
def close(self):
|
|
908
|
-
"""Close the client and release resources."""
|
|
931
|
+
"""Close the client, flush any buffered points, and release resources."""
|
|
932
|
+
if self.buffer_size() > 0:
|
|
933
|
+
try:
|
|
934
|
+
self.flush_buffer()
|
|
935
|
+
except Exception as e:
|
|
936
|
+
logger.debug("flush on close failed: %s", e)
|
|
909
937
|
if self._ws is not None:
|
|
910
938
|
self._ws.stop()
|
|
911
939
|
self._ws = None
|
|
@@ -28,11 +28,9 @@ from __future__ import annotations
|
|
|
28
28
|
import atexit
|
|
29
29
|
import json
|
|
30
30
|
import logging
|
|
31
|
-
import os
|
|
32
31
|
import queue
|
|
33
32
|
import random
|
|
34
33
|
import struct
|
|
35
|
-
import sys
|
|
36
34
|
import threading
|
|
37
35
|
import time
|
|
38
36
|
from dataclasses import dataclass, field
|
|
@@ -46,24 +44,9 @@ except ImportError as e: # pragma: no cover - import-time failure is obvious
|
|
|
46
44
|
"Install with: pip install websocket-client"
|
|
47
45
|
) from e
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# By default, print connection status to stderr so users running
|
|
52
|
-
# `python my_script.py` can see what's happening without having to
|
|
53
|
-
# configure the logging module. Set PLEXUS_QUIET=1 to disable.
|
|
54
|
-
_QUIET = os.environ.get("PLEXUS_QUIET", "").lower() in ("1", "true", "yes")
|
|
55
|
-
|
|
47
|
+
from plexus._log import _say
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
"""Single-line status message to stderr. Skipped if PLEXUS_QUIET=1."""
|
|
59
|
-
if _QUIET:
|
|
60
|
-
return
|
|
61
|
-
try:
|
|
62
|
-
sys.stderr.write(f"[plexus] {line}\n")
|
|
63
|
-
sys.stderr.flush()
|
|
64
|
-
except Exception:
|
|
65
|
-
# Stderr blew up — don't take the whole client down with it.
|
|
66
|
-
pass
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
67
50
|
|
|
68
51
|
AUTH_TIMEOUT_S = 10.0
|
|
69
52
|
HEARTBEAT_INTERVAL_S = 30.0
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Plexus
|
|
3
|
+
description: This skill should be used when the user asks to "set up Plexus on my device", "install Plexus on my Raspberry Pi", "send telemetry to Plexus", "integrate Plexus into my project", "build a monitoring app with Plexus", "query my Plexus devices", "stream live data from Plexus", or "connect to the Plexus API". Covers both the device-side SDK and the consumer-side REST/WebSocket API.
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Plexus
|
|
8
|
+
|
|
9
|
+
Plexus is a hardware observability platform. Devices push telemetry (metrics, events, video) via the Python SDK or raw HTTP. That data is then queryable and streamable through the Plexus Data API and dashboard.
|
|
10
|
+
|
|
11
|
+
There are two distinct workflows — choose based on what the user is building:
|
|
12
|
+
|
|
13
|
+
- **Device side** — install the SDK, authenticate, and start sending data from hardware
|
|
14
|
+
- **App side** — query historical data, stream live updates, and monitor a fleet via the REST/WebSocket API
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Device Side: SDK Setup
|
|
19
|
+
|
|
20
|
+
### Install
|
|
21
|
+
|
|
22
|
+
One-line setup for Linux devices (Raspberry Pi, Jetson, etc.) — installs the SDK, configures auth, and handles hardware support automatically:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
curl -sL https://app.plexus.company/setup | bash -s -- --key plx_xxx --name drone-01
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`--name` is required and must be unique across the fleet (`^[a-z0-9][a-z0-9_-]{1,62}$`). The gateway auto-suffixes duplicates (`drone-01_2`, etc.).
|
|
29
|
+
|
|
30
|
+
For development machines or manual installs:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install plexus-python
|
|
34
|
+
plexus init # opens browser to authenticate and saves key to ~/.plexus/config.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Send telemetry
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from plexus import Plexus
|
|
41
|
+
|
|
42
|
+
px = Plexus(source_id="drone-01") # reads PLEXUS_API_KEY from env or ~/.plexus/config.json
|
|
43
|
+
|
|
44
|
+
px.send("battery.voltage", 12.4)
|
|
45
|
+
px.send("motor.rpm", 3200, tags={"motor_id": "A1"})
|
|
46
|
+
px.send_batch([
|
|
47
|
+
("battery.voltage", 12.4),
|
|
48
|
+
("battery.current_ma", 340),
|
|
49
|
+
("motor.rpm", 3200),
|
|
50
|
+
])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Metric names use `<subsystem>.<metric>` convention (`battery.voltage`, `motor.rpm`, `gps.latitude`).
|
|
54
|
+
|
|
55
|
+
### Send events
|
|
56
|
+
|
|
57
|
+
Use `event()` for discrete occurrences — faults, state changes, operator actions. These appear as markers on the timeline, not time-series lines:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
px.event("fault", "E-stop triggered")
|
|
61
|
+
px.event("state_change", {"from": "IDLE", "to": "RUNNING"})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Group data into a run
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
with px.run("thermal-cycle-001"):
|
|
68
|
+
while running:
|
|
69
|
+
px.send("temperature", read_temp())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Reliability
|
|
73
|
+
|
|
74
|
+
Enable SQLite persistence to survive restarts and power loss:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
px = Plexus(persistent_buffer=True)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The SDK buffers locally, retries with exponential backoff, and syncs the device clock against the gateway on every WebSocket connection — important on embedded boards that boot without NTP.
|
|
81
|
+
|
|
82
|
+
### Video streaming
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# Frame by frame (OpenCV, picamera2)
|
|
86
|
+
import cv2
|
|
87
|
+
cap = cv2.VideoCapture(0)
|
|
88
|
+
while True:
|
|
89
|
+
ok, frame = cap.read()
|
|
90
|
+
if ok:
|
|
91
|
+
px.send_video_frame(frame, camera_id="front")
|
|
92
|
+
|
|
93
|
+
# From a URL (RTSP, HLS — requires FFmpeg on $PATH)
|
|
94
|
+
stop = px.stream_camera("rtsp://192.168.1.10/stream", camera_id="front")
|
|
95
|
+
stop.set() # stop when done
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## App Side: API Integration
|
|
101
|
+
|
|
102
|
+
**Base URL:** `https://api.plexus.company`
|
|
103
|
+
**Auth:** `x-api-key: plx_...` header on every request
|
|
104
|
+
**Keys:** [app.plexus.company/api](https://app.plexus.company/api)
|
|
105
|
+
|
|
106
|
+
### Discover devices
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
GET /v1/sources # all devices
|
|
110
|
+
GET /v1/sources?status=online # online only
|
|
111
|
+
GET /v1/sources/{source_id} # single device
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Discover metrics
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
GET /v1/sources/{source_id}/metrics # list metric names the device has reported
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Fetch latest values
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
GET /v1/sources/{source_id}/metrics/latest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Query historical data
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
GET /v1/sources/{source_id}/metrics/query?metrics=battery.voltage&last=1h
|
|
130
|
+
GET /v1/sources/{source_id}/metrics/query?metrics=cpu.percent&start=2026-05-01T00:00:00Z&end=2026-05-02T00:00:00Z
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Windows under ~10 min return raw `value` arrays. Longer windows auto-downsample to `min/max/avg/count` — Plexus picks an interval (`1m`, `10m`, `1h`, `1d`) targeting ~1000 points. Force with `?interval=raw` or `?interval=10m`.
|
|
134
|
+
|
|
135
|
+
### Stream live data (WebSocket)
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const ws = new WebSocket(
|
|
139
|
+
"wss://api.plexus.company/v1/sources/drone-01/metrics/stream?metrics=battery.voltage,motor.rpm"
|
|
140
|
+
);
|
|
141
|
+
ws.onopen = () => ws.send(JSON.stringify({ type: "auth", api_key: "plx_..." }));
|
|
142
|
+
ws.onmessage = (e) => {
|
|
143
|
+
const msg = JSON.parse(e.data);
|
|
144
|
+
if (msg.type === "telemetry") console.log(msg.points);
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Auth frame must be the **first frame** — socket closes with `4401` after 10 s if not received.
|
|
149
|
+
|
|
150
|
+
### Stream live video (WebSocket)
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const ws = new WebSocket(
|
|
154
|
+
"wss://api.plexus.company/v1/sources/drone-01/video/stream?camera_id=front"
|
|
155
|
+
);
|
|
156
|
+
ws.onopen = () => ws.send(JSON.stringify({ type: "auth", api_key: "plx_..." }));
|
|
157
|
+
ws.onmessage = (e) => {
|
|
158
|
+
const msg = JSON.parse(e.data);
|
|
159
|
+
if (msg.type === "video_frame") {
|
|
160
|
+
img.src = `data:image/jpeg;base64,${msg.frame}`;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Fleet-wide queries
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
GET /v1/fleet/health # total and online device counts
|
|
169
|
+
GET /v1/fleet/metrics?metric=cpu.percent&last=1h # one metric across all devices
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Key Conventions
|
|
175
|
+
|
|
176
|
+
- `source_id` is the stable device identity — everything (metrics, events, commands) is scoped to it
|
|
177
|
+
- API keys are prefixed `plx_` and scoped to an org
|
|
178
|
+
- Config persists to `~/.plexus/config.json`; SDK reads `PLEXUS_API_KEY` env var automatically
|
|
179
|
+
- Metric names: `<subsystem>.<metric>` — keep subsystems consistent across a fleet
|
|
180
|
+
- Tags: low-cardinality key-value pairs for filtering (`firmware`, `location`, `motor_id`) — not UUIDs or timestamps
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Additional Resources
|
|
185
|
+
|
|
186
|
+
For complete method signatures, options, and advanced patterns:
|
|
187
|
+
|
|
188
|
+
- **`references/sdk.md`** — Full SDK reference: all methods, transport options, clock correction, commands, environment variables
|
|
189
|
+
- **`references/api.md`** — Full API reference: all REST endpoints, WebSocket frame shapes, query params, close codes
|