plexus-python 0.1.0__tar.gz → 0.2.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 (93) hide show
  1. plexus_python-0.2.0/AGENTS.md +72 -0
  2. {plexus_python-0.1.0 → plexus_python-0.2.0}/API.md +26 -57
  3. plexus_python-0.2.0/CHANGELOG.md +32 -0
  4. plexus_python-0.2.0/PKG-INFO +140 -0
  5. plexus_python-0.2.0/README.md +108 -0
  6. plexus_python-0.2.0/examples/README.md +13 -0
  7. plexus_python-0.2.0/examples/basic.py +19 -0
  8. plexus_python-0.2.0/examples/can.py +40 -0
  9. plexus_python-0.2.0/examples/i2c_bme280.py +30 -0
  10. plexus_python-0.2.0/examples/mavlink.py +39 -0
  11. plexus_python-0.2.0/examples/mqtt.py +48 -0
  12. plexus_python-0.2.0/plexus/__init__.py +13 -0
  13. {plexus_python-0.1.0 → plexus_python-0.2.0}/plexus/client.py +4 -4
  14. {plexus_python-0.1.0 → plexus_python-0.2.0}/plexus/config.py +11 -29
  15. plexus_python-0.2.0/pyproject.toml +48 -0
  16. {plexus_python-0.1.0 → plexus_python-0.2.0}/scripts/setup.sh +1 -15
  17. plexus_python-0.1.0/AGENTS.md +0 -79
  18. plexus_python-0.1.0/CHANGELOG.md +0 -9
  19. plexus_python-0.1.0/PKG-INFO +0 -470
  20. plexus_python-0.1.0/README.md +0 -388
  21. plexus_python-0.1.0/examples/can_basic.py +0 -173
  22. plexus_python-0.1.0/examples/demo_field_unit.py +0 -254
  23. plexus_python-0.1.0/examples/demo_ground_station.py +0 -135
  24. plexus_python-0.1.0/examples/gateway_ble_relay.py +0 -97
  25. plexus_python-0.1.0/examples/mavlink_basic.py +0 -157
  26. plexus_python-0.1.0/plexus/__init__.py +0 -31
  27. plexus_python-0.1.0/plexus/__main__.py +0 -4
  28. plexus_python-0.1.0/plexus/adapters/__init__.py +0 -122
  29. plexus_python-0.1.0/plexus/adapters/base.py +0 -409
  30. plexus_python-0.1.0/plexus/adapters/ble.py +0 -257
  31. plexus_python-0.1.0/plexus/adapters/can.py +0 -439
  32. plexus_python-0.1.0/plexus/adapters/can_detect.py +0 -174
  33. plexus_python-0.1.0/plexus/adapters/mavlink.py +0 -642
  34. plexus_python-0.1.0/plexus/adapters/mavlink_detect.py +0 -192
  35. plexus_python-0.1.0/plexus/adapters/modbus.py +0 -622
  36. plexus_python-0.1.0/plexus/adapters/mqtt.py +0 -350
  37. plexus_python-0.1.0/plexus/adapters/opcua.py +0 -607
  38. plexus_python-0.1.0/plexus/adapters/registry.py +0 -206
  39. plexus_python-0.1.0/plexus/adapters/serial_adapter.py +0 -547
  40. plexus_python-0.1.0/plexus/cameras/__init__.py +0 -57
  41. plexus_python-0.1.0/plexus/cameras/auto.py +0 -239
  42. plexus_python-0.1.0/plexus/cameras/base.py +0 -189
  43. plexus_python-0.1.0/plexus/cameras/picamera.py +0 -171
  44. plexus_python-0.1.0/plexus/cameras/usb.py +0 -143
  45. plexus_python-0.1.0/plexus/cli.py +0 -783
  46. plexus_python-0.1.0/plexus/connector.py +0 -666
  47. plexus_python-0.1.0/plexus/deps.py +0 -246
  48. plexus_python-0.1.0/plexus/detect.py +0 -1238
  49. plexus_python-0.1.0/plexus/importers/__init__.py +0 -25
  50. plexus_python-0.1.0/plexus/importers/rosbag.py +0 -778
  51. plexus_python-0.1.0/plexus/sensors/__init__.py +0 -118
  52. plexus_python-0.1.0/plexus/sensors/ads1115.py +0 -164
  53. plexus_python-0.1.0/plexus/sensors/adxl345.py +0 -179
  54. plexus_python-0.1.0/plexus/sensors/auto.py +0 -290
  55. plexus_python-0.1.0/plexus/sensors/base.py +0 -412
  56. plexus_python-0.1.0/plexus/sensors/bh1750.py +0 -102
  57. plexus_python-0.1.0/plexus/sensors/bme280.py +0 -241
  58. plexus_python-0.1.0/plexus/sensors/gps.py +0 -317
  59. plexus_python-0.1.0/plexus/sensors/ina219.py +0 -149
  60. plexus_python-0.1.0/plexus/sensors/magnetometer.py +0 -239
  61. plexus_python-0.1.0/plexus/sensors/mpu6050.py +0 -162
  62. plexus_python-0.1.0/plexus/sensors/sht3x.py +0 -139
  63. plexus_python-0.1.0/plexus/sensors/spi_scan.py +0 -164
  64. plexus_python-0.1.0/plexus/sensors/system.py +0 -261
  65. plexus_python-0.1.0/plexus/sensors/vl53l0x.py +0 -109
  66. plexus_python-0.1.0/plexus/streaming.py +0 -743
  67. plexus_python-0.1.0/plexus/tui.py +0 -642
  68. plexus_python-0.1.0/pyproject.toml +0 -114
  69. plexus_python-0.1.0/tests/test_can_adapter.py +0 -332
  70. plexus_python-0.1.0/tests/test_connector.py +0 -30
  71. plexus_python-0.1.0/tests/test_gps.py +0 -147
  72. plexus_python-0.1.0/tests/test_mavlink_adapter.py +0 -711
  73. plexus_python-0.1.0/tests/test_modbus_adapter.py +0 -412
  74. plexus_python-0.1.0/tests/test_mqtt_adapter.py +0 -288
  75. plexus_python-0.1.0/tests/test_sensor_hub.py +0 -208
  76. plexus_python-0.1.0/tests/test_serial_adapter.py +0 -305
  77. {plexus_python-0.1.0 → plexus_python-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  78. {plexus_python-0.1.0 → plexus_python-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  79. {plexus_python-0.1.0 → plexus_python-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  80. {plexus_python-0.1.0 → plexus_python-0.2.0}/.github/workflows/ci.yml +0 -0
  81. {plexus_python-0.1.0 → plexus_python-0.2.0}/.github/workflows/publish.yml +0 -0
  82. {plexus_python-0.1.0 → plexus_python-0.2.0}/.gitignore +0 -0
  83. {plexus_python-0.1.0 → plexus_python-0.2.0}/CODE_OF_CONDUCT.md +0 -0
  84. {plexus_python-0.1.0 → plexus_python-0.2.0}/CONTRIBUTING.md +0 -0
  85. {plexus_python-0.1.0 → plexus_python-0.2.0}/LICENSE +0 -0
  86. {plexus_python-0.1.0 → plexus_python-0.2.0}/SECURITY.md +0 -0
  87. {plexus_python-0.1.0 → plexus_python-0.2.0}/plexus/buffer.py +0 -0
  88. {plexus_python-0.1.0 → plexus_python-0.2.0}/scripts/plexus.service +0 -0
  89. {plexus_python-0.1.0 → plexus_python-0.2.0}/scripts/scan_buses.py +0 -0
  90. {plexus_python-0.1.0 → plexus_python-0.2.0}/tests/test_basic.py +0 -0
  91. {plexus_python-0.1.0 → plexus_python-0.2.0}/tests/test_buffer.py +0 -0
  92. {plexus_python-0.1.0 → plexus_python-0.2.0}/tests/test_config.py +0 -0
  93. {plexus_python-0.1.0 → plexus_python-0.2.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,32 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - Thin SDK rewrite
4
+
5
+ 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()`.
6
+
7
+ ### Added
8
+
9
+ - 5 runnable example scripts: `basic.py`, `mavlink.py`, `can.py`, `mqtt.py`, `i2c_bme280.py`
10
+
11
+ ### Removed
12
+
13
+ - `plexus/adapters/` (MAVLink, CAN, MQTT, Modbus, OPC-UA, BLE, Serial — use the upstream lib directly)
14
+ - `plexus/sensors/` (I2C drivers + auto-detect — use Adafruit CircuitPython or smbus2 directly)
15
+ - `plexus/cameras/` (frame upload — out of scope)
16
+ - `plexus/cli.py`, `plexus/connector.py`, `plexus/streaming.py`, `plexus/detect.py`, `plexus/tui.py`, `plexus/deps.py`
17
+ - `plexus` console script, `python -m plexus`
18
+ - Extras: `[sensors]`, `[system]`, `[tui]`, `[mqtt]`, `[can]`, `[mavlink]`, `[modbus]`, `[opcua]`, `[ble]`, `[serial]`, `[ros]`, `[camera]`, `[picamera]`, `[all]`
19
+ - Runtime deps: `click`, `websockets`
20
+
21
+ ### Changed
22
+
23
+ - Default ingest endpoint points directly at the Plexus gateway (`https://plexus-gateway.fly.dev/ingest`), not the Next.js app proxy
24
+ - Client raises `ValueError` clearly when no API key is available, instead of invoking a login flow
25
+
26
+ ## [0.1.0] - Initial release
27
+
28
+ - `Plexus` thin client for HTTP ingest
29
+ - `plexus start` daemon with WebSocket streaming
30
+ - Protocol adapters: MAVLink, CAN, MQTT, Modbus, OPC-UA, Serial, BLE
31
+ - I2C sensor auto-detection and drivers
32
+ - Store-and-forward buffering (SQLite)
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: plexus-python
3
+ Version: 0.2.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
+ Provides-Extra: dev
28
+ Requires-Dist: pytest; extra == 'dev'
29
+ Requires-Dist: pytest-cov; extra == 'dev'
30
+ Requires-Dist: ruff; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # plexus-python
34
+
35
+ **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.
36
+
37
+ [![PyPI](https://img.shields.io/pypi/v/plexus-python)](https://pypi.org/project/plexus-python/)
38
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ pip install plexus-python
44
+ ```
45
+
46
+ ```python
47
+ from plexus import Plexus
48
+
49
+ px = Plexus(api_key="plx_xxx", source_id="device-001")
50
+ px.send("temperature", 72.5)
51
+ ```
52
+
53
+ Get an API key at [app.plexus.company](https://app.plexus.company) → Devices → Add Device.
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ from plexus import Plexus
59
+
60
+ px = Plexus(source_id="rig-01") # reads PLEXUS_API_KEY from env
61
+
62
+ # Numbers
63
+ px.send("engine.rpm", 3450)
64
+ px.send("coolant.temperature", 82.3, tags={"unit": "C"})
65
+
66
+ # Strings, bools, objects, arrays — all JSON-serializable
67
+ px.send("vehicle.state", "RUNNING")
68
+ px.send("motor.enabled", True)
69
+ px.send("position", {"x": 1.5, "y": 2.3, "z": 0.8})
70
+
71
+ # Batch
72
+ px.send_batch([
73
+ ("temperature", 72.5),
74
+ ("pressure", 1013.25),
75
+ ])
76
+
77
+ # Named run for grouping related data
78
+ with px.run("thermal-cycle-001"):
79
+ while running:
80
+ px.send("temperature", read_temp())
81
+ ```
82
+
83
+ ## Bring Your Own Protocol
84
+
85
+ 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()`.
86
+
87
+ ```python
88
+ # MAVLink (pymavlink)
89
+ for msg in conn:
90
+ if msg.get_type() == "ATTITUDE":
91
+ px.send("attitude.roll", msg.roll)
92
+
93
+ # CAN (python-can)
94
+ for msg in bus:
95
+ px.send(f"can.0x{msg.arbitration_id:x}", int.from_bytes(msg.data, "big"))
96
+
97
+ # MQTT (paho-mqtt)
98
+ def on_message(_c, _u, msg):
99
+ px.send(msg.topic.replace("/", "."), float(msg.payload))
100
+
101
+ # I2C sensor (Adafruit CircuitPython)
102
+ px.send("temperature", bme.temperature)
103
+ ```
104
+
105
+ See [`examples/`](examples/) for runnable versions of each.
106
+
107
+ ## Reliability
108
+
109
+ 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:
110
+
111
+ ```python
112
+ px = Plexus(persistent_buffer=True)
113
+ ```
114
+
115
+ Point counts and flush:
116
+
117
+ ```python
118
+ px.buffer_size()
119
+ px.flush_buffer()
120
+ ```
121
+
122
+ ## Environment Variables
123
+
124
+ | Variable | Description | Default |
125
+ | ----------------------- | ---------------------------- | -------------------------------- |
126
+ | `PLEXUS_API_KEY` | API key (required) | none |
127
+ | `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://plexus-gateway.fly.dev` |
128
+ | `PLEXUS_GATEWAY_WS_URL` | WebSocket URL (unused in SDK, for compatibility) | `wss://plexus-gateway.fly.dev` |
129
+
130
+ ## Architecture
131
+
132
+ ```
133
+ Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
134
+ ```
135
+
136
+ 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.
137
+
138
+ ## License
139
+
140
+ Apache 2.0
@@ -0,0 +1,108 @@
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
+ ## Environment Variables
91
+
92
+ | Variable | Description | Default |
93
+ | ----------------------- | ---------------------------- | -------------------------------- |
94
+ | `PLEXUS_API_KEY` | API key (required) | none |
95
+ | `PLEXUS_GATEWAY_URL` | HTTP ingest URL | `https://plexus-gateway.fly.dev` |
96
+ | `PLEXUS_GATEWAY_WS_URL` | WebSocket URL (unused in SDK, for compatibility) | `wss://plexus-gateway.fly.dev` |
97
+
98
+ ## Architecture
99
+
100
+ ```
101
+ Your code ── px.send() ── HTTP POST /ingest ──> plexus-gateway ──> ClickHouse + Dashboard
102
+ ```
103
+
104
+ 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.
105
+
106
+ ## License
107
+
108
+ 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)
@@ -0,0 +1,39 @@
1
+ """
2
+ Pipe MAVLink telemetry into Plexus.
3
+
4
+ Prereq:
5
+ pip install plexus-python pymavlink
6
+ export PLEXUS_API_KEY=plx_xxx
7
+
8
+ Run:
9
+ python mavlink.py udpin:0.0.0.0:14550
10
+ """
11
+
12
+ import sys
13
+
14
+ from pymavlink import mavutil
15
+
16
+ from plexus import Plexus
17
+
18
+ conn_str = sys.argv[1] if len(sys.argv) > 1 else "udpin:0.0.0.0:14550"
19
+ px = Plexus(source_id="drone-001")
20
+ conn = mavutil.mavlink_connection(conn_str)
21
+ conn.wait_heartbeat()
22
+
23
+ while True:
24
+ msg = conn.recv_match(blocking=True)
25
+ if msg is None:
26
+ continue
27
+ t = msg.get_type()
28
+
29
+ if t == "ATTITUDE":
30
+ px.send("attitude.roll", msg.roll)
31
+ px.send("attitude.pitch", msg.pitch)
32
+ px.send("attitude.yaw", msg.yaw)
33
+ elif t == "GLOBAL_POSITION_INT":
34
+ px.send("gps.lat", msg.lat / 1e7)
35
+ px.send("gps.lon", msg.lon / 1e7)
36
+ px.send("gps.alt_m", msg.alt / 1000.0)
37
+ elif t == "SYS_STATUS":
38
+ px.send("battery.voltage", msg.voltage_battery / 1000.0)
39
+ px.send("battery.current", msg.current_battery / 100.0)
@@ -0,0 +1,48 @@
1
+ """
2
+ Bridge an MQTT broker into Plexus.
3
+
4
+ Prereq:
5
+ pip install plexus-python paho-mqtt
6
+ export PLEXUS_API_KEY=plx_xxx
7
+
8
+ Run:
9
+ python mqtt.py localhost sensors/#
10
+ """
11
+
12
+ import json
13
+ import sys
14
+
15
+ import paho.mqtt.client as mqtt
16
+
17
+ from plexus import Plexus
18
+
19
+ broker = sys.argv[1] if len(sys.argv) > 1 else "localhost"
20
+ topic = sys.argv[2] if len(sys.argv) > 2 else "sensors/#"
21
+
22
+ px = Plexus(source_id="mqtt-gateway")
23
+
24
+
25
+ def on_message(_client, _userdata, msg):
26
+ name = msg.topic.replace("/", ".")
27
+ payload = msg.payload.decode("utf-8", errors="replace")
28
+ try:
29
+ data = json.loads(payload)
30
+ except ValueError:
31
+ try:
32
+ px.send(name, float(payload))
33
+ except ValueError:
34
+ px.send(name, payload)
35
+ return
36
+
37
+ if isinstance(data, dict):
38
+ for key, value in data.items():
39
+ px.send(f"{name}.{key}", value)
40
+ else:
41
+ px.send(name, data)
42
+
43
+
44
+ client = mqtt.Client()
45
+ client.on_message = on_message
46
+ client.connect(broker)
47
+ client.subscribe(topic)
48
+ client.loop_forever()