plexus-python 0.1.0__py3-none-any.whl
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/__init__.py +31 -0
- plexus/__main__.py +4 -0
- plexus/adapters/__init__.py +122 -0
- plexus/adapters/base.py +409 -0
- plexus/adapters/ble.py +257 -0
- plexus/adapters/can.py +439 -0
- plexus/adapters/can_detect.py +174 -0
- plexus/adapters/mavlink.py +642 -0
- plexus/adapters/mavlink_detect.py +192 -0
- plexus/adapters/modbus.py +622 -0
- plexus/adapters/mqtt.py +350 -0
- plexus/adapters/opcua.py +607 -0
- plexus/adapters/registry.py +206 -0
- plexus/adapters/serial_adapter.py +547 -0
- plexus/buffer.py +257 -0
- plexus/cameras/__init__.py +57 -0
- plexus/cameras/auto.py +239 -0
- plexus/cameras/base.py +189 -0
- plexus/cameras/picamera.py +171 -0
- plexus/cameras/usb.py +143 -0
- plexus/cli.py +783 -0
- plexus/client.py +465 -0
- plexus/config.py +169 -0
- plexus/connector.py +666 -0
- plexus/deps.py +246 -0
- plexus/detect.py +1238 -0
- plexus/importers/__init__.py +25 -0
- plexus/importers/rosbag.py +778 -0
- plexus/sensors/__init__.py +118 -0
- plexus/sensors/ads1115.py +164 -0
- plexus/sensors/adxl345.py +179 -0
- plexus/sensors/auto.py +290 -0
- plexus/sensors/base.py +412 -0
- plexus/sensors/bh1750.py +102 -0
- plexus/sensors/bme280.py +241 -0
- plexus/sensors/gps.py +317 -0
- plexus/sensors/ina219.py +149 -0
- plexus/sensors/magnetometer.py +239 -0
- plexus/sensors/mpu6050.py +162 -0
- plexus/sensors/sht3x.py +139 -0
- plexus/sensors/spi_scan.py +164 -0
- plexus/sensors/system.py +261 -0
- plexus/sensors/vl53l0x.py +109 -0
- plexus/streaming.py +743 -0
- plexus/tui.py +642 -0
- plexus_python-0.1.0.dist-info/METADATA +470 -0
- plexus_python-0.1.0.dist-info/RECORD +50 -0
- plexus_python-0.1.0.dist-info/WHEEL +4 -0
- plexus_python-0.1.0.dist-info/entry_points.txt +2 -0
- plexus_python-0.1.0.dist-info/licenses/LICENSE +190 -0
plexus/adapters/mqtt.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MQTT Protocol Adapter - Bridge MQTT brokers to Plexus
|
|
3
|
+
|
|
4
|
+
This adapter subscribes to MQTT topics and forwards messages as metrics.
|
|
5
|
+
Topic names are converted to metric names (slashes become dots).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from plexus.adapters import MQTTAdapter
|
|
9
|
+
|
|
10
|
+
# Basic usage
|
|
11
|
+
adapter = MQTTAdapter(broker="localhost", topic="sensors/#")
|
|
12
|
+
|
|
13
|
+
# With authentication
|
|
14
|
+
adapter = MQTTAdapter(
|
|
15
|
+
broker="mqtt.example.com",
|
|
16
|
+
port=8883,
|
|
17
|
+
username="user",
|
|
18
|
+
password="pass",
|
|
19
|
+
use_tls=True,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Run with callback
|
|
23
|
+
def handle_data(metrics):
|
|
24
|
+
for m in metrics:
|
|
25
|
+
print(f"{m.name}: {m.value}")
|
|
26
|
+
|
|
27
|
+
adapter.run(on_data=handle_data)
|
|
28
|
+
|
|
29
|
+
Requires: pip install plexus-python[mqtt]
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import time
|
|
35
|
+
from typing import Any, List, Optional
|
|
36
|
+
|
|
37
|
+
from plexus.adapters.base import (
|
|
38
|
+
ProtocolAdapter,
|
|
39
|
+
Metric,
|
|
40
|
+
AdapterConfig,
|
|
41
|
+
AdapterState,
|
|
42
|
+
)
|
|
43
|
+
from plexus.adapters.registry import AdapterRegistry
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MQTTAdapter(ProtocolAdapter):
|
|
49
|
+
"""
|
|
50
|
+
MQTT protocol adapter.
|
|
51
|
+
|
|
52
|
+
Subscribes to MQTT topics and converts messages to Plexus metrics.
|
|
53
|
+
Supports JSON payloads (each key becomes a metric) and simple numeric values.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
broker: MQTT broker hostname
|
|
57
|
+
port: MQTT broker port (default: 1883)
|
|
58
|
+
topic: Topic pattern to subscribe to (default: "#" for all)
|
|
59
|
+
username: Optional username for authentication
|
|
60
|
+
password: Optional password for authentication
|
|
61
|
+
use_tls: Whether to use TLS encryption
|
|
62
|
+
client_id: Optional MQTT client ID
|
|
63
|
+
prefix: Topic prefix to strip from metric names
|
|
64
|
+
qos: Quality of service level (0, 1, or 2)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
broker: str = "localhost",
|
|
70
|
+
port: int = 1883,
|
|
71
|
+
topic: str = "#",
|
|
72
|
+
username: Optional[str] = None,
|
|
73
|
+
password: Optional[str] = None,
|
|
74
|
+
use_tls: bool = False,
|
|
75
|
+
client_id: Optional[str] = None,
|
|
76
|
+
prefix: str = "",
|
|
77
|
+
qos: int = 0,
|
|
78
|
+
**kwargs,
|
|
79
|
+
):
|
|
80
|
+
config = AdapterConfig(
|
|
81
|
+
name="mqtt",
|
|
82
|
+
params={
|
|
83
|
+
"broker": broker,
|
|
84
|
+
"port": port,
|
|
85
|
+
"topic": topic,
|
|
86
|
+
"username": username,
|
|
87
|
+
"use_tls": use_tls,
|
|
88
|
+
"prefix": prefix,
|
|
89
|
+
"qos": qos,
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
super().__init__(config)
|
|
93
|
+
|
|
94
|
+
self.broker = broker
|
|
95
|
+
self.port = port
|
|
96
|
+
self.topic = topic
|
|
97
|
+
self.username = username
|
|
98
|
+
self.password = password
|
|
99
|
+
self.use_tls = use_tls
|
|
100
|
+
self.client_id = client_id
|
|
101
|
+
self.prefix = prefix
|
|
102
|
+
self.qos = qos
|
|
103
|
+
|
|
104
|
+
self._client = None
|
|
105
|
+
self._pending_metrics: List[Metric] = []
|
|
106
|
+
|
|
107
|
+
def _check_mqtt_installed(self) -> None:
|
|
108
|
+
"""Check if paho-mqtt is installed."""
|
|
109
|
+
import importlib.util
|
|
110
|
+
if importlib.util.find_spec("paho.mqtt.client") is None:
|
|
111
|
+
raise ImportError(
|
|
112
|
+
"MQTT support not installed. Run: pip install plexus-python[mqtt]"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def connect(self) -> bool:
|
|
116
|
+
"""Connect to the MQTT broker."""
|
|
117
|
+
self._check_mqtt_installed()
|
|
118
|
+
import paho.mqtt.client as mqtt
|
|
119
|
+
|
|
120
|
+
self._set_state(AdapterState.CONNECTING)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Create client
|
|
124
|
+
if hasattr(mqtt, 'CallbackAPIVersion'):
|
|
125
|
+
# paho-mqtt 2.0+
|
|
126
|
+
self._client = mqtt.Client(
|
|
127
|
+
mqtt.CallbackAPIVersion.VERSION2,
|
|
128
|
+
client_id=self.client_id,
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
# paho-mqtt 1.x
|
|
132
|
+
self._client = mqtt.Client(client_id=self.client_id)
|
|
133
|
+
|
|
134
|
+
# Set up callbacks
|
|
135
|
+
self._client.on_connect = self._on_connect
|
|
136
|
+
self._client.on_message = self._on_message
|
|
137
|
+
self._client.on_disconnect = self._on_disconnect
|
|
138
|
+
|
|
139
|
+
# Authentication
|
|
140
|
+
if self.username:
|
|
141
|
+
self._client.username_pw_set(self.username, self.password)
|
|
142
|
+
|
|
143
|
+
# TLS
|
|
144
|
+
if self.use_tls:
|
|
145
|
+
self._client.tls_set()
|
|
146
|
+
|
|
147
|
+
# Connect
|
|
148
|
+
self._client.connect(self.broker, self.port, keepalive=60)
|
|
149
|
+
self._client.loop_start()
|
|
150
|
+
|
|
151
|
+
# Wait for connection (with timeout)
|
|
152
|
+
timeout = 10
|
|
153
|
+
start = time.time()
|
|
154
|
+
while self._state == AdapterState.CONNECTING:
|
|
155
|
+
if time.time() - start > timeout:
|
|
156
|
+
self._set_state(
|
|
157
|
+
AdapterState.ERROR,
|
|
158
|
+
f"Connection timeout after {timeout}s"
|
|
159
|
+
)
|
|
160
|
+
return False
|
|
161
|
+
time.sleep(0.1)
|
|
162
|
+
|
|
163
|
+
return self._state == AdapterState.CONNECTED
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self._set_state(AdapterState.ERROR, str(e))
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
def disconnect(self) -> None:
|
|
170
|
+
"""Disconnect from the MQTT broker."""
|
|
171
|
+
if self._client:
|
|
172
|
+
try:
|
|
173
|
+
self._client.loop_stop()
|
|
174
|
+
self._client.disconnect()
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
self._client = None
|
|
178
|
+
self._set_state(AdapterState.DISCONNECTED)
|
|
179
|
+
|
|
180
|
+
def poll(self) -> List[Metric]:
|
|
181
|
+
"""
|
|
182
|
+
Get pending metrics (collected from MQTT messages).
|
|
183
|
+
|
|
184
|
+
Returns collected metrics and clears the buffer.
|
|
185
|
+
"""
|
|
186
|
+
metrics = self._pending_metrics.copy()
|
|
187
|
+
self._pending_metrics.clear()
|
|
188
|
+
return metrics
|
|
189
|
+
|
|
190
|
+
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
|
191
|
+
"""Handle MQTT connection."""
|
|
192
|
+
# paho-mqtt 2.0+ uses ReasonCode objects; 1.x uses ints
|
|
193
|
+
is_success = (
|
|
194
|
+
not rc.is_failure if hasattr(rc, 'is_failure') else rc == 0
|
|
195
|
+
)
|
|
196
|
+
if is_success:
|
|
197
|
+
self._set_state(AdapterState.CONNECTED)
|
|
198
|
+
client.subscribe(self.topic, qos=self.qos)
|
|
199
|
+
logger.info(f"Connected to MQTT broker, subscribed to '{self.topic}'")
|
|
200
|
+
else:
|
|
201
|
+
self._set_state(AdapterState.ERROR, f"Connection refused: {rc}")
|
|
202
|
+
|
|
203
|
+
def _on_disconnect(self, client, userdata, rc, properties=None):
|
|
204
|
+
"""Handle MQTT disconnection with auto-reconnect."""
|
|
205
|
+
if rc != 0:
|
|
206
|
+
logger.warning(f"Unexpected MQTT disconnect (rc={rc}), attempting reconnect...")
|
|
207
|
+
self._set_state(
|
|
208
|
+
AdapterState.RECONNECTING,
|
|
209
|
+
f"Unexpected disconnect: {rc}"
|
|
210
|
+
)
|
|
211
|
+
if self.config.auto_reconnect:
|
|
212
|
+
import threading
|
|
213
|
+
threading.Thread(
|
|
214
|
+
target=self._mqtt_reconnect_loop,
|
|
215
|
+
daemon=True,
|
|
216
|
+
).start()
|
|
217
|
+
else:
|
|
218
|
+
logger.info("MQTT disconnected cleanly")
|
|
219
|
+
self._set_state(AdapterState.DISCONNECTED)
|
|
220
|
+
|
|
221
|
+
def _mqtt_reconnect_loop(self):
|
|
222
|
+
"""Reconnect to MQTT broker with exponential backoff."""
|
|
223
|
+
import random
|
|
224
|
+
attempt = 0
|
|
225
|
+
delay = self.config.reconnect_interval
|
|
226
|
+
max_attempts = self.config.max_reconnect_attempts
|
|
227
|
+
|
|
228
|
+
while attempt < max_attempts:
|
|
229
|
+
attempt += 1
|
|
230
|
+
jitter = random.uniform(0.75, 1.25)
|
|
231
|
+
time.sleep(delay * jitter)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
if self._client:
|
|
235
|
+
self._client.reconnect()
|
|
236
|
+
logger.info("MQTT reconnected after %d attempt(s)", attempt)
|
|
237
|
+
return
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.warning("MQTT reconnect attempt %d failed: %s", attempt, e)
|
|
240
|
+
|
|
241
|
+
delay = min(delay * 2, 60.0)
|
|
242
|
+
|
|
243
|
+
self._set_state(AdapterState.ERROR, "Max MQTT reconnect attempts reached")
|
|
244
|
+
|
|
245
|
+
def _on_message(self, client, userdata, msg):
|
|
246
|
+
"""Handle incoming MQTT message."""
|
|
247
|
+
try:
|
|
248
|
+
metrics = self._parse_message(msg.topic, msg.payload)
|
|
249
|
+
if metrics:
|
|
250
|
+
self._pending_metrics.extend(metrics)
|
|
251
|
+
self._emit_data(metrics)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.warning(f"Failed to parse MQTT message: {e}")
|
|
254
|
+
|
|
255
|
+
def _parse_message(self, topic: str, payload: bytes) -> List[Metric]:
|
|
256
|
+
"""
|
|
257
|
+
Parse MQTT message into metrics.
|
|
258
|
+
|
|
259
|
+
Handles:
|
|
260
|
+
- JSON objects: Each key becomes a metric
|
|
261
|
+
- Simple numeric values: Topic becomes metric name
|
|
262
|
+
- Strings: Forwarded as string metrics
|
|
263
|
+
"""
|
|
264
|
+
metrics = []
|
|
265
|
+
|
|
266
|
+
# Convert topic to metric name
|
|
267
|
+
metric_name = topic
|
|
268
|
+
if self.prefix and metric_name.startswith(self.prefix):
|
|
269
|
+
metric_name = metric_name[len(self.prefix):]
|
|
270
|
+
metric_name = metric_name.replace("/", ".").strip(".")
|
|
271
|
+
|
|
272
|
+
# Decode payload
|
|
273
|
+
try:
|
|
274
|
+
payload_str = payload.decode("utf-8").strip()
|
|
275
|
+
except UnicodeDecodeError:
|
|
276
|
+
return [] # Skip binary payloads
|
|
277
|
+
|
|
278
|
+
if not payload_str:
|
|
279
|
+
return []
|
|
280
|
+
|
|
281
|
+
# Try JSON first
|
|
282
|
+
if payload_str.startswith("{") or payload_str.startswith("["):
|
|
283
|
+
try:
|
|
284
|
+
data = json.loads(payload_str)
|
|
285
|
+
|
|
286
|
+
if isinstance(data, dict):
|
|
287
|
+
# Each key becomes a metric
|
|
288
|
+
for key, value in data.items():
|
|
289
|
+
if self._is_valid_value(value):
|
|
290
|
+
full_name = f"{metric_name}.{key}" if metric_name else key
|
|
291
|
+
metrics.append(Metric(full_name, value))
|
|
292
|
+
elif isinstance(data, list):
|
|
293
|
+
# Array becomes array metric
|
|
294
|
+
metrics.append(Metric(metric_name, data))
|
|
295
|
+
return metrics
|
|
296
|
+
|
|
297
|
+
except json.JSONDecodeError:
|
|
298
|
+
pass # Fall through to simple value handling
|
|
299
|
+
|
|
300
|
+
# Try numeric value
|
|
301
|
+
try:
|
|
302
|
+
value = float(payload_str)
|
|
303
|
+
# Convert to int if it's a whole number
|
|
304
|
+
if value.is_integer():
|
|
305
|
+
value = int(value)
|
|
306
|
+
metrics.append(Metric(metric_name, value))
|
|
307
|
+
return metrics
|
|
308
|
+
except ValueError:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
# Treat as string value
|
|
312
|
+
metrics.append(Metric(metric_name, payload_str))
|
|
313
|
+
return metrics
|
|
314
|
+
|
|
315
|
+
def _is_valid_value(self, value: Any) -> bool:
|
|
316
|
+
"""Check if value is valid for a metric."""
|
|
317
|
+
if isinstance(value, (int, float)):
|
|
318
|
+
return True
|
|
319
|
+
if isinstance(value, str):
|
|
320
|
+
return True
|
|
321
|
+
if isinstance(value, bool):
|
|
322
|
+
return True
|
|
323
|
+
if isinstance(value, (dict, list)):
|
|
324
|
+
return True
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
def _run_loop(self) -> None:
|
|
328
|
+
"""
|
|
329
|
+
Run loop for MQTT (push-based).
|
|
330
|
+
|
|
331
|
+
MQTT uses callbacks so we just need to keep the loop alive.
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
while self.is_connected:
|
|
335
|
+
time.sleep(0.1)
|
|
336
|
+
except KeyboardInterrupt:
|
|
337
|
+
pass
|
|
338
|
+
finally:
|
|
339
|
+
self.disconnect()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# Register the adapter
|
|
343
|
+
AdapterRegistry.register(
|
|
344
|
+
"mqtt",
|
|
345
|
+
MQTTAdapter,
|
|
346
|
+
description="Bridge MQTT brokers to Plexus",
|
|
347
|
+
author="Plexus",
|
|
348
|
+
version="1.0.0",
|
|
349
|
+
requires=["paho-mqtt"],
|
|
350
|
+
)
|