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.
Files changed (50) hide show
  1. plexus/__init__.py +31 -0
  2. plexus/__main__.py +4 -0
  3. plexus/adapters/__init__.py +122 -0
  4. plexus/adapters/base.py +409 -0
  5. plexus/adapters/ble.py +257 -0
  6. plexus/adapters/can.py +439 -0
  7. plexus/adapters/can_detect.py +174 -0
  8. plexus/adapters/mavlink.py +642 -0
  9. plexus/adapters/mavlink_detect.py +192 -0
  10. plexus/adapters/modbus.py +622 -0
  11. plexus/adapters/mqtt.py +350 -0
  12. plexus/adapters/opcua.py +607 -0
  13. plexus/adapters/registry.py +206 -0
  14. plexus/adapters/serial_adapter.py +547 -0
  15. plexus/buffer.py +257 -0
  16. plexus/cameras/__init__.py +57 -0
  17. plexus/cameras/auto.py +239 -0
  18. plexus/cameras/base.py +189 -0
  19. plexus/cameras/picamera.py +171 -0
  20. plexus/cameras/usb.py +143 -0
  21. plexus/cli.py +783 -0
  22. plexus/client.py +465 -0
  23. plexus/config.py +169 -0
  24. plexus/connector.py +666 -0
  25. plexus/deps.py +246 -0
  26. plexus/detect.py +1238 -0
  27. plexus/importers/__init__.py +25 -0
  28. plexus/importers/rosbag.py +778 -0
  29. plexus/sensors/__init__.py +118 -0
  30. plexus/sensors/ads1115.py +164 -0
  31. plexus/sensors/adxl345.py +179 -0
  32. plexus/sensors/auto.py +290 -0
  33. plexus/sensors/base.py +412 -0
  34. plexus/sensors/bh1750.py +102 -0
  35. plexus/sensors/bme280.py +241 -0
  36. plexus/sensors/gps.py +317 -0
  37. plexus/sensors/ina219.py +149 -0
  38. plexus/sensors/magnetometer.py +239 -0
  39. plexus/sensors/mpu6050.py +162 -0
  40. plexus/sensors/sht3x.py +139 -0
  41. plexus/sensors/spi_scan.py +164 -0
  42. plexus/sensors/system.py +261 -0
  43. plexus/sensors/vl53l0x.py +109 -0
  44. plexus/streaming.py +743 -0
  45. plexus/tui.py +642 -0
  46. plexus_python-0.1.0.dist-info/METADATA +470 -0
  47. plexus_python-0.1.0.dist-info/RECORD +50 -0
  48. plexus_python-0.1.0.dist-info/WHEEL +4 -0
  49. plexus_python-0.1.0.dist-info/entry_points.txt +2 -0
  50. plexus_python-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -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
+ )