plexus-python 0.1.0__tar.gz → 0.3.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.
Files changed (95) hide show
  1. plexus_python-0.3.0/AGENTS.md +72 -0
  2. {plexus_python-0.1.0 → plexus_python-0.3.0}/API.md +26 -57
  3. plexus_python-0.3.0/CHANGELOG.md +45 -0
  4. plexus_python-0.3.0/PKG-INFO +176 -0
  5. plexus_python-0.3.0/README.md +142 -0
  6. plexus_python-0.3.0/examples/README.md +13 -0
  7. plexus_python-0.3.0/examples/basic.py +19 -0
  8. plexus_python-0.3.0/examples/can.py +40 -0
  9. plexus_python-0.3.0/examples/i2c_bme280.py +30 -0
  10. plexus_python-0.3.0/examples/mavlink.py +39 -0
  11. plexus_python-0.3.0/examples/mqtt.py +48 -0
  12. plexus_python-0.3.0/plexus/__init__.py +14 -0
  13. {plexus_python-0.1.0 → plexus_python-0.3.0}/plexus/client.py +74 -8
  14. {plexus_python-0.1.0 → plexus_python-0.3.0}/plexus/config.py +11 -29
  15. plexus_python-0.3.0/plexus/ws.py +344 -0
  16. plexus_python-0.3.0/pyproject.toml +49 -0
  17. {plexus_python-0.1.0 → plexus_python-0.3.0}/scripts/setup.sh +1 -15
  18. plexus_python-0.3.0/tests/test_ws.py +255 -0
  19. plexus_python-0.1.0/AGENTS.md +0 -79
  20. plexus_python-0.1.0/CHANGELOG.md +0 -9
  21. plexus_python-0.1.0/PKG-INFO +0 -470
  22. plexus_python-0.1.0/README.md +0 -388
  23. plexus_python-0.1.0/examples/can_basic.py +0 -173
  24. plexus_python-0.1.0/examples/demo_field_unit.py +0 -254
  25. plexus_python-0.1.0/examples/demo_ground_station.py +0 -135
  26. plexus_python-0.1.0/examples/gateway_ble_relay.py +0 -97
  27. plexus_python-0.1.0/examples/mavlink_basic.py +0 -157
  28. plexus_python-0.1.0/plexus/__init__.py +0 -31
  29. plexus_python-0.1.0/plexus/__main__.py +0 -4
  30. plexus_python-0.1.0/plexus/adapters/__init__.py +0 -122
  31. plexus_python-0.1.0/plexus/adapters/base.py +0 -409
  32. plexus_python-0.1.0/plexus/adapters/ble.py +0 -257
  33. plexus_python-0.1.0/plexus/adapters/can.py +0 -439
  34. plexus_python-0.1.0/plexus/adapters/can_detect.py +0 -174
  35. plexus_python-0.1.0/plexus/adapters/mavlink.py +0 -642
  36. plexus_python-0.1.0/plexus/adapters/mavlink_detect.py +0 -192
  37. plexus_python-0.1.0/plexus/adapters/modbus.py +0 -622
  38. plexus_python-0.1.0/plexus/adapters/mqtt.py +0 -350
  39. plexus_python-0.1.0/plexus/adapters/opcua.py +0 -607
  40. plexus_python-0.1.0/plexus/adapters/registry.py +0 -206
  41. plexus_python-0.1.0/plexus/adapters/serial_adapter.py +0 -547
  42. plexus_python-0.1.0/plexus/cameras/__init__.py +0 -57
  43. plexus_python-0.1.0/plexus/cameras/auto.py +0 -239
  44. plexus_python-0.1.0/plexus/cameras/base.py +0 -189
  45. plexus_python-0.1.0/plexus/cameras/picamera.py +0 -171
  46. plexus_python-0.1.0/plexus/cameras/usb.py +0 -143
  47. plexus_python-0.1.0/plexus/cli.py +0 -783
  48. plexus_python-0.1.0/plexus/connector.py +0 -666
  49. plexus_python-0.1.0/plexus/deps.py +0 -246
  50. plexus_python-0.1.0/plexus/detect.py +0 -1238
  51. plexus_python-0.1.0/plexus/importers/__init__.py +0 -25
  52. plexus_python-0.1.0/plexus/importers/rosbag.py +0 -778
  53. plexus_python-0.1.0/plexus/sensors/__init__.py +0 -118
  54. plexus_python-0.1.0/plexus/sensors/ads1115.py +0 -164
  55. plexus_python-0.1.0/plexus/sensors/adxl345.py +0 -179
  56. plexus_python-0.1.0/plexus/sensors/auto.py +0 -290
  57. plexus_python-0.1.0/plexus/sensors/base.py +0 -412
  58. plexus_python-0.1.0/plexus/sensors/bh1750.py +0 -102
  59. plexus_python-0.1.0/plexus/sensors/bme280.py +0 -241
  60. plexus_python-0.1.0/plexus/sensors/gps.py +0 -317
  61. plexus_python-0.1.0/plexus/sensors/ina219.py +0 -149
  62. plexus_python-0.1.0/plexus/sensors/magnetometer.py +0 -239
  63. plexus_python-0.1.0/plexus/sensors/mpu6050.py +0 -162
  64. plexus_python-0.1.0/plexus/sensors/sht3x.py +0 -139
  65. plexus_python-0.1.0/plexus/sensors/spi_scan.py +0 -164
  66. plexus_python-0.1.0/plexus/sensors/system.py +0 -261
  67. plexus_python-0.1.0/plexus/sensors/vl53l0x.py +0 -109
  68. plexus_python-0.1.0/plexus/streaming.py +0 -743
  69. plexus_python-0.1.0/plexus/tui.py +0 -642
  70. plexus_python-0.1.0/pyproject.toml +0 -114
  71. plexus_python-0.1.0/tests/test_can_adapter.py +0 -332
  72. plexus_python-0.1.0/tests/test_connector.py +0 -30
  73. plexus_python-0.1.0/tests/test_gps.py +0 -147
  74. plexus_python-0.1.0/tests/test_mavlink_adapter.py +0 -711
  75. plexus_python-0.1.0/tests/test_modbus_adapter.py +0 -412
  76. plexus_python-0.1.0/tests/test_mqtt_adapter.py +0 -288
  77. plexus_python-0.1.0/tests/test_sensor_hub.py +0 -208
  78. plexus_python-0.1.0/tests/test_serial_adapter.py +0 -305
  79. {plexus_python-0.1.0 → plexus_python-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  80. {plexus_python-0.1.0 → plexus_python-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  81. {plexus_python-0.1.0 → plexus_python-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  82. {plexus_python-0.1.0 → plexus_python-0.3.0}/.github/workflows/ci.yml +0 -0
  83. {plexus_python-0.1.0 → plexus_python-0.3.0}/.github/workflows/publish.yml +0 -0
  84. {plexus_python-0.1.0 → plexus_python-0.3.0}/.gitignore +0 -0
  85. {plexus_python-0.1.0 → plexus_python-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  86. {plexus_python-0.1.0 → plexus_python-0.3.0}/CONTRIBUTING.md +0 -0
  87. {plexus_python-0.1.0 → plexus_python-0.3.0}/LICENSE +0 -0
  88. {plexus_python-0.1.0 → plexus_python-0.3.0}/SECURITY.md +0 -0
  89. {plexus_python-0.1.0 → plexus_python-0.3.0}/plexus/buffer.py +0 -0
  90. {plexus_python-0.1.0 → plexus_python-0.3.0}/scripts/plexus.service +0 -0
  91. {plexus_python-0.1.0 → plexus_python-0.3.0}/scripts/scan_buses.py +0 -0
  92. {plexus_python-0.1.0 → plexus_python-0.3.0}/tests/test_basic.py +0 -0
  93. {plexus_python-0.1.0 → plexus_python-0.3.0}/tests/test_buffer.py +0 -0
  94. {plexus_python-0.1.0 → plexus_python-0.3.0}/tests/test_config.py +0 -0
  95. {plexus_python-0.1.0 → plexus_python-0.3.0}/tests/test_retry.py +0 -0
@@ -0,0 +1,72 @@
1
+ # AGENTS.md — plexus-python
2
+
3
+ Machine-readable interface for AI assistants and automation scripts.
4
+
5
+ ## Environment Variables
6
+
7
+ | Variable | Description | Default |
8
+ | ----------------------- | -------------------------------------------- | -------------------------------- |
9
+ | `PLEXUS_API_KEY` | API key for authentication (required) | none |
10
+ | `PLEXUS_GATEWAY_URL` | Gateway HTTP ingest URL | `https://plexus-gateway.fly.dev` |
11
+ | `PLEXUS_GATEWAY_WS_URL` | Gateway WebSocket URL | `wss://plexus-gateway.fly.dev` |
12
+
13
+ ## CLI Commands
14
+
15
+ ```bash
16
+ plexus start # Stream with PLEXUS_API_KEY env var
17
+ plexus start --key plx_xxxxx # Pass key inline
18
+ plexus start --device-id my-device # Set device identifier
19
+ plexus reset # Clear config
20
+ ```
21
+
22
+ An API key is required — set `PLEXUS_API_KEY` or pass `--key`. There is no interactive signup; get a key at app.plexus.company/devices.
23
+
24
+ ## Exit Codes
25
+
26
+ | Code | Meaning |
27
+ | ----- | ------------------------------------- |
28
+ | `0` | Clean shutdown |
29
+ | `1` | Configuration or authentication error |
30
+ | `130` | Interrupted (SIGINT / Ctrl+C) |
31
+
32
+ ## Python SDK
33
+
34
+ ```python
35
+ from plexus import Plexus
36
+
37
+ px = Plexus(api_key="plx_xxxxx", source_id="device-001")
38
+ px.send("temperature", 72.5)
39
+ px.send("pressure", 1013.25, tags={"unit": "hPa"})
40
+
41
+ # Batch
42
+ px.send_batch([
43
+ ("temperature", 72.5),
44
+ ("pressure", 1013.25),
45
+ ])
46
+
47
+ # Persistent buffering for reliability
48
+ px = Plexus(api_key="plx_xxxxx", persistent_buffer=True)
49
+ ```
50
+
51
+ ## Project Structure
52
+
53
+ ```
54
+ plexus/
55
+ ├── cli.py # CLI entry point (click commands)
56
+ ├── tui.py # Live terminal dashboard (Rich)
57
+ ├── client.py # Plexus HTTP client (thin SDK)
58
+ ├── connector.py # WebSocket daemon
59
+ ├── config.py # Config file + env var management
60
+ ├── detect.py # Hardware auto-detection
61
+ ├── deps.py # Dependency helpers
62
+ ├── sensors/ # I2C sensor drivers + SensorHub
63
+ └── adapters/ # Protocol adapters: CAN, MAVLink, MQTT, Modbus, OPC-UA, BLE, Serial
64
+ ```
65
+
66
+ ## Key Conventions
67
+
68
+ - Config lives in `~/.plexus/config.json`
69
+ - API keys are prefixed with `plx_`
70
+ - Source IDs (device slugs) namespace metrics
71
+ - HTTP ingest → `POST /ingest` on gateway; WebSocket → `/ws/device` for streaming + commands
72
+ - Gateway resolves `org_id` server-side from the API key — clients do not supply it
@@ -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://app.plexus.company/api/ingest \
33
+ curl -X POST https://plexus-gateway.fly.dev/ingest \
34
34
  -H "x-api-key: YOUR_API_KEY" \
35
35
  -H "Content-Type: application/json" \
36
36
  -d '{
@@ -280,7 +280,7 @@ import requests
280
280
  import time
281
281
 
282
282
  requests.post(
283
- "https://app.plexus.company/api/ingest",
283
+ "https://plexus-gateway.fly.dev/ingest",
284
284
  headers={"x-api-key": "plx_xxxxx"},
285
285
  json={
286
286
  "points": [{
@@ -296,7 +296,7 @@ requests.post(
296
296
  ### JavaScript
297
297
 
298
298
  ```javascript
299
- await fetch("https://app.plexus.company/api/ingest", {
299
+ await fetch("https://plexus-gateway.fly.dev/ingest", {
300
300
  method: "POST",
301
301
  headers: {
302
302
  "x-api-key": "plx_xxxxx",
@@ -338,7 +338,7 @@ func main() {
338
338
  }
339
339
 
340
340
  body, _ := json.Marshal(points)
341
- req, _ := http.NewRequest("POST", "https://app.plexus.company/api/ingest", bytes.NewBuffer(body))
341
+ req, _ := http.NewRequest("POST", "https://plexus-gateway.fly.dev/ingest", bytes.NewBuffer(body))
342
342
  req.Header.Set("x-api-key", "plx_xxxxx")
343
343
  req.Header.Set("Content-Type", "application/json")
344
344
 
@@ -354,7 +354,7 @@ func main() {
354
354
 
355
355
  void sendToPlexus(const char* metric, float value) {
356
356
  HTTPClient http;
357
- http.begin("https://app.plexus.company/api/ingest");
357
+ http.begin("https://plexus-gateway.fly.dev/ingest");
358
358
  http.addHeader("Content-Type", "application/json");
359
359
  http.addHeader("x-api-key", "plx_xxxxx");
360
360
 
@@ -377,7 +377,7 @@ void sendToPlexus(const char* metric, float value) {
377
377
  API_KEY="plx_xxxxx"
378
378
  SOURCE_ID="sensor-001"
379
379
 
380
- curl -X POST https://app.plexus.company/api/ingest \
380
+ curl -X POST https://plexus-gateway.fly.dev/ingest \
381
381
  -H "x-api-key: $API_KEY" \
382
382
  -H "Content-Type: application/json" \
383
383
  -d "{
@@ -390,69 +390,38 @@ curl -X POST https://app.plexus.company/api/ingest \
390
390
  }"
391
391
  ```
392
392
 
393
- ## Protocol Adapters
393
+ ## Bring Your Own Protocol
394
394
 
395
- Plexus supports protocol adapters for ingesting data from various sources.
396
-
397
- ### CAN Bus Adapter
398
-
399
- Read CAN bus data with optional DBC signal decoding:
400
-
401
- ```bash
402
- pip install plexus-python[can]
403
- ```
395
+ For MAVLink, CAN, MQTT, Modbus, OPC-UA, BLE, Serial, or any other protocol, use the upstream Python library directly and pass values to `px.send()`. Plexus stays out of your decode path:
404
396
 
405
397
  ```python
406
- from plexus.adapters import CANAdapter
398
+ # CAN example — using python-can directly
399
+ import can
407
400
  from plexus import Plexus
408
401
 
409
- plexus = Plexus(api_key="plx_xxx", source_id="vehicle-001")
410
- adapter = CANAdapter(
411
- interface="socketcan",
412
- channel="can0",
413
- dbc_path="vehicle.dbc", # Optional: decode signals
414
- )
402
+ px = Plexus(api_key="plx_xxx", source_id="vehicle-001")
403
+ bus = can.interface.Bus(channel="can0", bustype="socketcan")
415
404
 
416
- with adapter:
417
- while True:
418
- for metric in adapter.poll():
419
- # Raw: can.raw.0x123 = "DEADBEEF"
420
- # Decoded: engine_rpm = 2500
421
- plexus.send(metric.name, metric.value, tags=metric.tags)
405
+ for msg in bus:
406
+ px.send(f"can.0x{msg.arbitration_id:x}", int.from_bytes(msg.data, "big"))
422
407
  ```
423
408
 
424
- **Setup virtual CAN for testing (Linux):**
425
-
426
- ```bash
427
- sudo modprobe vcan
428
- sudo ip link add dev vcan0 type vcan
429
- sudo ip link set up vcan0
430
-
431
- # Send test frames
432
- cansend vcan0 123#DEADBEEF
433
- ```
434
-
435
- **Supported interfaces:** socketcan, pcan, vector, kvaser, slcan, virtual
436
-
437
- ### MQTT Adapter
409
+ ```python
410
+ # MAVLink example — using pymavlink directly
411
+ from pymavlink import mavutil
412
+ from plexus import Plexus
438
413
 
439
- Bridge MQTT brokers to Plexus:
414
+ px = Plexus(api_key="plx_xxx", source_id="drone-001")
415
+ conn = mavutil.mavlink_connection("udpin:0.0.0.0:14550")
440
416
 
441
- ```bash
442
- pip install plexus-python[mqtt]
417
+ while True:
418
+ msg = conn.recv_match(blocking=True)
419
+ if msg.get_type() == "ATTITUDE":
420
+ px.send("attitude.roll", msg.roll)
421
+ px.send("attitude.pitch", msg.pitch)
443
422
  ```
444
423
 
445
- ```python
446
- from plexus.adapters import MQTTAdapter
447
-
448
- adapter = MQTTAdapter(
449
- broker="localhost",
450
- topic="sensors/#",
451
- port=1883,
452
- )
453
- adapter.connect()
454
- adapter.run(on_data=my_callback)
455
- ```
424
+ See [docs.plexus.dev/recipes](https://docs.plexus.dev/recipes) for more.
456
425
 
457
426
  ## Python SDK with Sensor Drivers
458
427
 
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0] - WebSocket transport
4
+
5
+ Adds a wire-compatible WebSocket transport matching the `plexus-c` SDK. WS is now the default; failed sends transparently fall back to `POST /ingest`.
6
+
7
+ ### Added
8
+
9
+ - `plexus.WebSocketTransport` — connects to `/ws/device` on the gateway. Exchanges the same `device_auth` / `authenticated` / `telemetry` / `heartbeat` / `typed_command` / `command_result` frames as `plexus-c`.
10
+ - `Plexus(transport="ws" | "http")` — defaults to `"ws"`.
11
+ - `Plexus.on_command(name, handler, description=..., params=...)` — register command handlers; automatic `ack`, handler return becomes `result`, exceptions become `error`.
12
+ - `Plexus.close()` — stops the WebSocket thread.
13
+ - Runtime dep: `websocket-client>=1.7`.
14
+ - Tests: `tests/test_ws.py` (auth handshake, telemetry, command roundtrip, error paths).
15
+
16
+ ## [0.2.0] - Thin SDK rewrite
17
+
18
+ Breaking. `plexus-python` is now just the thin client — no agent, adapters, sensors, CLI, or TUI. The package is 886 lines with one runtime dependency (`requests`). Protocol integrations (MAVLink, CAN, MQTT, Modbus, OPC-UA, BLE, I2C sensors) now live as standalone recipes in `examples/`, using the upstream library directly (`pymavlink`, `python-can`, `paho-mqtt`, etc.) plus `px.send()`.
19
+
20
+ ### Added
21
+
22
+ - 5 runnable example scripts: `basic.py`, `mavlink.py`, `can.py`, `mqtt.py`, `i2c_bme280.py`
23
+
24
+ ### Removed
25
+
26
+ - `plexus/adapters/` (MAVLink, CAN, MQTT, Modbus, OPC-UA, BLE, Serial — use the upstream lib directly)
27
+ - `plexus/sensors/` (I2C drivers + auto-detect — use Adafruit CircuitPython or smbus2 directly)
28
+ - `plexus/cameras/` (frame upload — out of scope)
29
+ - `plexus/cli.py`, `plexus/connector.py`, `plexus/streaming.py`, `plexus/detect.py`, `plexus/tui.py`, `plexus/deps.py`
30
+ - `plexus` console script, `python -m plexus`
31
+ - Extras: `[sensors]`, `[system]`, `[tui]`, `[mqtt]`, `[can]`, `[mavlink]`, `[modbus]`, `[opcua]`, `[ble]`, `[serial]`, `[ros]`, `[camera]`, `[picamera]`, `[all]`
32
+ - Runtime deps: `click`, `websockets`
33
+
34
+ ### Changed
35
+
36
+ - Default ingest endpoint points directly at the Plexus gateway (`https://plexus-gateway.fly.dev/ingest`), not the Next.js app proxy
37
+ - Client raises `ValueError` clearly when no API key is available, instead of invoking a login flow
38
+
39
+ ## [0.1.0] - Initial release
40
+
41
+ - `Plexus` thin client for HTTP ingest
42
+ - `plexus start` daemon with WebSocket streaming
43
+ - Protocol adapters: MAVLink, CAN, MQTT, Modbus, OPC-UA, Serial, BLE
44
+ - I2C sensor auto-detection and drivers
45
+ - Store-and-forward buffering (SQLite)
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: plexus-python
3
+ Version: 0.3.0
4
+ Summary: Thin Python SDK for Plexus — send telemetry in one line
5
+ Project-URL: Homepage, https://plexus.dev
6
+ Project-URL: Documentation, https://docs.plexus.dev
7
+ Project-URL: Repository, https://github.com/plexus-oss/plexus-python
8
+ Project-URL: Issues, https://github.com/plexus-oss/plexus-python/issues
9
+ Author-email: Plexus <hello@plexus.dev>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: fleet,hardware,iot,monitoring,observability,telemetry
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Topic :: System :: Hardware
25
+ Requires-Python: >=3.8
26
+ Requires-Dist: requests>=2.28.0
27
+ Requires-Dist: websocket-client>=1.7
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == 'dev'
30
+ Requires-Dist: pytest-cov; extra == 'dev'
31
+ Requires-Dist: ruff; extra == 'dev'
32
+ Requires-Dist: websockets>=12; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # plexus-python
36
+
37
+ **Thin Python SDK for [Plexus](https://plexus.company).** Send telemetry to the Plexus gateway in one line. Storage, dashboards, alerts, and fleet management live in the platform — this package just ships your data.
38
+
39
+ [![PyPI](https://img.shields.io/pypi/v/plexus-python)](https://pypi.org/project/plexus-python/)
40
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
41
+
42
+ ## Quick Start
43
+
44
+ ```bash
45
+ pip install plexus-python
46
+ ```
47
+
48
+ ```python
49
+ from plexus import Plexus
50
+
51
+ px = Plexus(api_key="plx_xxx", source_id="device-001")
52
+ px.send("temperature", 72.5)
53
+ ```
54
+
55
+ Get an API key at [app.plexus.company](https://app.plexus.company) → Devices → Add Device.
56
+
57
+ ## Usage
58
+
59
+ ```python
60
+ from plexus import Plexus
61
+
62
+ px = Plexus(source_id="rig-01") # reads PLEXUS_API_KEY from env
63
+
64
+ # Numbers
65
+ px.send("engine.rpm", 3450)
66
+ px.send("coolant.temperature", 82.3, tags={"unit": "C"})
67
+
68
+ # Strings, bools, objects, arrays — all JSON-serializable
69
+ px.send("vehicle.state", "RUNNING")
70
+ px.send("motor.enabled", True)
71
+ px.send("position", {"x": 1.5, "y": 2.3, "z": 0.8})
72
+
73
+ # Batch
74
+ px.send_batch([
75
+ ("temperature", 72.5),
76
+ ("pressure", 1013.25),
77
+ ])
78
+
79
+ # Named run for grouping related data
80
+ with px.run("thermal-cycle-001"):
81
+ while running:
82
+ px.send("temperature", read_temp())
83
+ ```
84
+
85
+ ## Bring Your Own Protocol
86
+
87
+ This package ships no adapters, auto-detection, or daemons — just the client. Use whatever library you'd use anyway and pipe values into `px.send()`.
88
+
89
+ ```python
90
+ # MAVLink (pymavlink)
91
+ for msg in conn:
92
+ if msg.get_type() == "ATTITUDE":
93
+ px.send("attitude.roll", msg.roll)
94
+
95
+ # CAN (python-can)
96
+ for msg in bus:
97
+ px.send(f"can.0x{msg.arbitration_id:x}", int.from_bytes(msg.data, "big"))
98
+
99
+ # MQTT (paho-mqtt)
100
+ def on_message(_c, _u, msg):
101
+ px.send(msg.topic.replace("/", "."), float(msg.payload))
102
+
103
+ # I2C sensor (Adafruit CircuitPython)
104
+ px.send("temperature", bme.temperature)
105
+ ```
106
+
107
+ See [`examples/`](examples/) for runnable versions of each.
108
+
109
+ ## Reliability
110
+
111
+ Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. Enable SQLite persistence to survive restarts and power loss:
112
+
113
+ ```python
114
+ px = Plexus(persistent_buffer=True)
115
+ ```
116
+
117
+ Point counts and flush:
118
+
119
+ ```python
120
+ px.buffer_size()
121
+ px.flush_buffer()
122
+ ```
123
+
124
+ ## Transport
125
+
126
+ By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — same wire protocol as the C SDK. This gives you:
127
+
128
+ - lower-latency streaming of telemetry,
129
+ - live command delivery from the UI / API to the device.
130
+
131
+ If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
132
+
133
+ ```python
134
+ # default — ws with http fallback
135
+ px = Plexus()
136
+
137
+ # force http (legacy)
138
+ px = Plexus(transport="http")
139
+ ```
140
+
141
+ ### Handling commands
142
+
143
+ Register a handler before the first `send()` so the command is advertised in the auth frame:
144
+
145
+ ```python
146
+ def reboot(name, params):
147
+ delay = params.get("delay_s", 0)
148
+ # ... reboot logic ...
149
+ return {"ok": True, "delay": delay}
150
+
151
+ px = Plexus()
152
+ px.on_command("reboot", reboot, description="reboot the device")
153
+ px.send("temperature", 72.5) # opens the socket, waits for auth
154
+ ```
155
+
156
+ The SDK sends an `ack` frame before invoking the handler, then a `result` frame with whatever the handler returns (or an `error` frame if it raises).
157
+
158
+ ## Environment Variables
159
+
160
+ | Variable | Description | Default |
161
+ | ----------------------- | ---------------------------- | -------------------------------- |
162
+ | `PLEXUS_API_KEY` | API key (required) | none |
163
+ | `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://plexus-gateway.fly.dev` |
164
+ | `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://plexus-gateway.fly.dev` |
165
+
166
+ ## Architecture
167
+
168
+ ```
169
+ Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
170
+ ```
171
+
172
+ One thin path. No agent, no daemon, no adapters. If you want the full HardwareOps platform — dashboards, alerts, RCA, fleet views — that's the web UI at app.plexus.company. This package gets your data there.
173
+
174
+ ## License
175
+
176
+ Apache 2.0
@@ -0,0 +1,142 @@
1
+ # plexus-python
2
+
3
+ **Thin Python SDK for [Plexus](https://plexus.company).** Send telemetry to the Plexus gateway in one line. Storage, dashboards, alerts, and fleet management live in the platform — this package just ships your data.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/plexus-python)](https://pypi.org/project/plexus-python/)
6
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
7
+
8
+ ## Quick Start
9
+
10
+ ```bash
11
+ pip install plexus-python
12
+ ```
13
+
14
+ ```python
15
+ from plexus import Plexus
16
+
17
+ px = Plexus(api_key="plx_xxx", source_id="device-001")
18
+ px.send("temperature", 72.5)
19
+ ```
20
+
21
+ Get an API key at [app.plexus.company](https://app.plexus.company) → Devices → Add Device.
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from plexus import Plexus
27
+
28
+ px = Plexus(source_id="rig-01") # reads PLEXUS_API_KEY from env
29
+
30
+ # Numbers
31
+ px.send("engine.rpm", 3450)
32
+ px.send("coolant.temperature", 82.3, tags={"unit": "C"})
33
+
34
+ # Strings, bools, objects, arrays — all JSON-serializable
35
+ px.send("vehicle.state", "RUNNING")
36
+ px.send("motor.enabled", True)
37
+ px.send("position", {"x": 1.5, "y": 2.3, "z": 0.8})
38
+
39
+ # Batch
40
+ px.send_batch([
41
+ ("temperature", 72.5),
42
+ ("pressure", 1013.25),
43
+ ])
44
+
45
+ # Named run for grouping related data
46
+ with px.run("thermal-cycle-001"):
47
+ while running:
48
+ px.send("temperature", read_temp())
49
+ ```
50
+
51
+ ## Bring Your Own Protocol
52
+
53
+ This package ships no adapters, auto-detection, or daemons — just the client. Use whatever library you'd use anyway and pipe values into `px.send()`.
54
+
55
+ ```python
56
+ # MAVLink (pymavlink)
57
+ for msg in conn:
58
+ if msg.get_type() == "ATTITUDE":
59
+ px.send("attitude.roll", msg.roll)
60
+
61
+ # CAN (python-can)
62
+ for msg in bus:
63
+ px.send(f"can.0x{msg.arbitration_id:x}", int.from_bytes(msg.data, "big"))
64
+
65
+ # MQTT (paho-mqtt)
66
+ def on_message(_c, _u, msg):
67
+ px.send(msg.topic.replace("/", "."), float(msg.payload))
68
+
69
+ # I2C sensor (Adafruit CircuitPython)
70
+ px.send("temperature", bme.temperature)
71
+ ```
72
+
73
+ See [`examples/`](examples/) for runnable versions of each.
74
+
75
+ ## Reliability
76
+
77
+ Every send buffers locally before hitting the network, retries with exponential backoff, and keeps your data safe across outages. Enable SQLite persistence to survive restarts and power loss:
78
+
79
+ ```python
80
+ px = Plexus(persistent_buffer=True)
81
+ ```
82
+
83
+ Point counts and flush:
84
+
85
+ ```python
86
+ px.buffer_size()
87
+ px.flush_buffer()
88
+ ```
89
+
90
+ ## Transport
91
+
92
+ By default the SDK connects over a **WebSocket** to `/ws/device` on the gateway — same wire protocol as the C SDK. This gives you:
93
+
94
+ - lower-latency streaming of telemetry,
95
+ - live command delivery from the UI / API to the device.
96
+
97
+ If the socket is unavailable, sends transparently fall back to `POST /ingest` so no data is lost.
98
+
99
+ ```python
100
+ # default — ws with http fallback
101
+ px = Plexus()
102
+
103
+ # force http (legacy)
104
+ px = Plexus(transport="http")
105
+ ```
106
+
107
+ ### Handling commands
108
+
109
+ Register a handler before the first `send()` so the command is advertised in the auth frame:
110
+
111
+ ```python
112
+ def reboot(name, params):
113
+ delay = params.get("delay_s", 0)
114
+ # ... reboot logic ...
115
+ return {"ok": True, "delay": delay}
116
+
117
+ px = Plexus()
118
+ px.on_command("reboot", reboot, description="reboot the device")
119
+ px.send("temperature", 72.5) # opens the socket, waits for auth
120
+ ```
121
+
122
+ The SDK sends an `ack` frame before invoking the handler, then a `result` frame with whatever the handler returns (or an `error` frame if it raises).
123
+
124
+ ## Environment Variables
125
+
126
+ | Variable | Description | Default |
127
+ | ----------------------- | ---------------------------- | -------------------------------- |
128
+ | `PLEXUS_API_KEY` | API key (required) | none |
129
+ | `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://plexus-gateway.fly.dev` |
130
+ | `PLEXUS_GATEWAY_WS_URL` | WebSocket URL | `wss://plexus-gateway.fly.dev` |
131
+
132
+ ## Architecture
133
+
134
+ ```
135
+ Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
136
+ ```
137
+
138
+ One thin path. No agent, no daemon, no adapters. If you want the full HardwareOps platform — dashboards, alerts, RCA, fleet views — that's the web UI at app.plexus.company. This package gets your data there.
139
+
140
+ ## License
141
+
142
+ Apache 2.0
@@ -0,0 +1,13 @@
1
+ # Examples
2
+
3
+ Each script is standalone — copy into your project, adjust the `source_id`, and run. Every example takes `PLEXUS_API_KEY` from the environment.
4
+
5
+ | Script | What it shows | Extra deps |
6
+ | --- | --- | --- |
7
+ | `basic.py` | The 3-line starter | none |
8
+ | `mavlink.py` | Drone / autopilot telemetry | `pymavlink` |
9
+ | `can.py` | Vehicle CAN bus (with optional DBC decode) | `python-can`, `cantools` |
10
+ | `mqtt.py` | MQTT broker → Plexus bridge | `paho-mqtt` |
11
+ | `i2c_bme280.py` | Raspberry Pi environmental sensor | `adafruit-circuitpython-bme280` |
12
+
13
+ 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,19 @@
1
+ """
2
+ Basic telemetry — the 3-line starter.
3
+
4
+ Run:
5
+ export PLEXUS_API_KEY=plx_xxx
6
+ python basic.py
7
+ """
8
+
9
+ import random
10
+ import time
11
+
12
+ from plexus import Plexus
13
+
14
+ px = Plexus(source_id="demo-device")
15
+
16
+ while True:
17
+ px.send("temperature", 20 + random.random() * 5)
18
+ px.send("humidity", 40 + random.random() * 10)
19
+ time.sleep(1)
@@ -0,0 +1,40 @@
1
+ """
2
+ Pipe CAN bus frames into Plexus.
3
+
4
+ Prereq:
5
+ pip install plexus-python python-can cantools
6
+ sudo ip link set can0 type can bitrate 500000 && sudo ip link set can0 up
7
+ export PLEXUS_API_KEY=plx_xxx
8
+
9
+ Run:
10
+ python can.py can0 [vehicle.dbc]
11
+ """
12
+
13
+ import sys
14
+
15
+ import can
16
+ import cantools
17
+
18
+ from plexus import Plexus
19
+
20
+ channel = sys.argv[1] if len(sys.argv) > 1 else "can0"
21
+ dbc_path = sys.argv[2] if len(sys.argv) > 2 else None
22
+
23
+ px = Plexus(source_id="vehicle-001")
24
+ bus = can.interface.Bus(channel=channel, bustype="socketcan")
25
+ db = cantools.database.load_file(dbc_path) if dbc_path else None
26
+
27
+ for msg in bus:
28
+ if db:
29
+ try:
30
+ decoded = db.decode_message(msg.arbitration_id, msg.data)
31
+ for name, value in decoded.items():
32
+ if isinstance(value, (int, float)):
33
+ px.send(name, value)
34
+ continue
35
+ except KeyError:
36
+ pass
37
+ px.send(
38
+ f"can.raw.0x{msg.arbitration_id:x}",
39
+ int.from_bytes(msg.data, "big"),
40
+ )
@@ -0,0 +1,30 @@
1
+ """
2
+ Read a BME280 over I2C on a Raspberry Pi and stream to Plexus.
3
+
4
+ Prereq:
5
+ pip install plexus-python adafruit-circuitpython-bme280
6
+ sudo usermod -aG i2c $USER && reboot # (once)
7
+ export PLEXUS_API_KEY=plx_xxx
8
+
9
+ Run:
10
+ python i2c_bme280.py
11
+ """
12
+
13
+ import time
14
+
15
+ import board
16
+ import busio
17
+ from adafruit_bme280 import basic as adafruit_bme280
18
+
19
+ from plexus import Plexus
20
+
21
+ i2c = busio.I2C(board.SCL, board.SDA)
22
+ bme = adafruit_bme280.Adafruit_BME280_I2C(i2c)
23
+
24
+ px = Plexus(source_id="pi-lab-01")
25
+
26
+ while True:
27
+ px.send("temperature", bme.temperature)
28
+ px.send("humidity", bme.relative_humidity)
29
+ px.send("pressure", bme.pressure)
30
+ time.sleep(2)