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.
Files changed (48) hide show
  1. {plexus_python-0.5.1 → plexus_python-0.5.2}/CHANGELOG.md +28 -0
  2. {plexus_python-0.5.1 → plexus_python-0.5.2}/PKG-INFO +1 -1
  3. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/uv.lock +3 -3
  4. {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/__init__.py +1 -1
  5. plexus_python-0.5.2/plexus/_log.py +15 -0
  6. {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/client.py +53 -25
  7. {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/ws.py +2 -19
  8. {plexus_python-0.5.1 → plexus_python-0.5.2}/pyproject.toml +1 -1
  9. plexus_python-0.5.2/skills/plexus/SKILL.md +189 -0
  10. plexus_python-0.5.2/skills/plexus/references/api.md +331 -0
  11. plexus_python-0.5.2/skills/plexus/references/sdk.md +227 -0
  12. plexus_python-0.5.2/tests/test_basic.py +114 -0
  13. {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_retry.py +2 -0
  14. plexus_python-0.5.1/tests/test_basic.py +0 -61
  15. {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  17. {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/workflows/ci.yml +0 -0
  19. {plexus_python-0.5.1 → plexus_python-0.5.2}/.github/workflows/publish.yml +0 -0
  20. {plexus_python-0.5.1 → plexus_python-0.5.2}/.gitignore +0 -0
  21. {plexus_python-0.5.1 → plexus_python-0.5.2}/AGENTS.md +0 -0
  22. {plexus_python-0.5.1 → plexus_python-0.5.2}/API.md +0 -0
  23. {plexus_python-0.5.1 → plexus_python-0.5.2}/CODE_OF_CONDUCT.md +0 -0
  24. {plexus_python-0.5.1 → plexus_python-0.5.2}/CONTRIBUTING.md +0 -0
  25. {plexus_python-0.5.1 → plexus_python-0.5.2}/LICENSE +0 -0
  26. {plexus_python-0.5.1 → plexus_python-0.5.2}/README.md +0 -0
  27. {plexus_python-0.5.1 → plexus_python-0.5.2}/SECURITY.md +0 -0
  28. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/.python-version +0 -0
  29. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/README.md +0 -0
  30. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/basic.py +0 -0
  31. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/can.py +0 -0
  32. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/i2c_bme280.py +0 -0
  33. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/mac_metrics.py +0 -0
  34. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/mavlink.py +0 -0
  35. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/mqtt.py +0 -0
  36. {plexus_python-0.5.1 → plexus_python-0.5.2}/examples/pyproject.toml +0 -0
  37. {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/buffer.py +0 -0
  38. {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/cli.py +0 -0
  39. {plexus_python-0.5.1 → plexus_python-0.5.2}/plexus/config.py +0 -0
  40. {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/plexus.service +0 -0
  41. {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/release.sh +0 -0
  42. {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/scan_buses.py +0 -0
  43. {plexus_python-0.5.1 → plexus_python-0.5.2}/scripts/setup.sh +0 -0
  44. {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_buffer.py +0 -0
  45. {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_config.py +0 -0
  46. {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_video.py +0 -0
  47. {plexus_python-0.5.1 → plexus_python-0.5.2}/tests/test_ws.py +0 -0
  48. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python
3
- Version: 0.5.1
3
+ Version: 0.5.2
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
@@ -674,11 +674,11 @@ wheels = [
674
674
 
675
675
  [[package]]
676
676
  name = "urllib3"
677
- version = "2.6.3"
677
+ version = "2.7.0"
678
678
  source = { registry = "https://pypi.org/simple" }
679
- sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
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/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
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.1"
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 start' or set PLEXUS_API_KEY.
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 os
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 = False,
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) tuples. Values can be any FlexValue type.
372
- timestamp: Shared timestamp for all points. If not provided, uses current time.
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
- ts_ms = self._normalize_ts_ms(timestamp)
387
- data_points = [self._make_point(m, v, ts_ms, tags) for m, v in 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 start' or set PLEXUS_API_KEY"
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
- logger = logging.getLogger(__name__)
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
- def _say(line: str) -> None:
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "plexus-python"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "Thin Python SDK for Plexus — send telemetry in one line"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.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