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,118 @@
1
+ """
2
+ Plexus Sensor Drivers
3
+
4
+ Pre-built drivers for common sensors that stream data to Plexus.
5
+
6
+ Quick Start:
7
+ from plexus import Plexus
8
+ from plexus.sensors import MPU6050
9
+
10
+ px = Plexus()
11
+ imu = MPU6050()
12
+
13
+ while True:
14
+ for reading in imu.read():
15
+ px.send(reading.metric, reading.value)
16
+
17
+ With SensorHub (recommended):
18
+ from plexus import Plexus
19
+ from plexus.sensors import SensorHub, MPU6050, BME280
20
+
21
+ hub = SensorHub()
22
+ hub.add(MPU6050(sample_rate=100))
23
+ hub.add(BME280(sample_rate=1))
24
+ hub.run(Plexus())
25
+
26
+ Auto-detection:
27
+ from plexus import Plexus
28
+ from plexus.sensors import auto_sensors
29
+
30
+ hub = auto_sensors() # Finds all connected sensors
31
+ hub.run(Plexus())
32
+
33
+ Supported Sensors:
34
+ - MPU6050: 6-axis IMU (accelerometer + gyroscope)
35
+ - MPU9250: 9-axis IMU (accelerometer + gyroscope + magnetometer)
36
+ - BME280: Environmental (temperature, humidity, pressure)
37
+ - INA219: Current/power monitor (voltage, current, power)
38
+ - SHT3x: Precision temperature + humidity (with CRC verification)
39
+ - BH1750: Ambient light sensor (1-65535 lux)
40
+ - VL53L0X: Time-of-flight distance sensor (30-2000mm)
41
+ - ADS1115: 16-bit ADC (4 channels, programmable gain)
42
+ - QMC5883L: 3-axis magnetometer / compass
43
+ - HMC5883L: 3-axis magnetometer / compass
44
+ - ADXL345: 3-axis accelerometer (I2C or SPI)
45
+ - GPSSensor: GPS receiver (lat, lon, altitude, speed via NMEA serial)
46
+ - SystemSensor: System health (CPU temp, memory, disk, load)
47
+ """
48
+
49
+ from .base import BaseSensor, SensorReading, SensorHub
50
+ from .mpu6050 import MPU6050, MPU9250
51
+ from .bme280 import BME280
52
+ from .ina219 import INA219
53
+ from .sht3x import SHT3x
54
+ from .bh1750 import BH1750
55
+ from .vl53l0x import VL53L0X
56
+ from .ads1115 import ADS1115
57
+ from .magnetometer import QMC5883L, HMC5883L
58
+ from .adxl345 import ADXL345
59
+ from .gps import GPSSensor
60
+ from .auto import scan_sensors, auto_sensors, scan_i2c, DetectedSensor
61
+ from .system import SystemSensor
62
+
63
+ try:
64
+ from .spi_scan import scan_spi, scan_spi_buses, SPISensorMatch
65
+ except ImportError:
66
+ pass
67
+
68
+ __all__ = [
69
+ # Base classes
70
+ "BaseSensor",
71
+ "SensorReading",
72
+ "SensorHub",
73
+ # I2C sensors
74
+ "MPU6050",
75
+ "MPU9250",
76
+ "BME280",
77
+ "INA219",
78
+ "SHT3x",
79
+ "BH1750",
80
+ "VL53L0X",
81
+ "ADS1115",
82
+ "QMC5883L",
83
+ "HMC5883L",
84
+ "ADXL345",
85
+ # Serial sensors
86
+ "GPSSensor",
87
+ # System
88
+ "SystemSensor",
89
+ # Auto-detection
90
+ "scan_sensors",
91
+ "auto_sensors",
92
+ "scan_i2c",
93
+ "DetectedSensor",
94
+ "scan_spi",
95
+ "scan_spi_buses",
96
+ "SPISensorMatch",
97
+ # Registry
98
+ "SENSOR_REGISTRY",
99
+ ]
100
+
101
+ # Maps sensor names to driver classes (used by config and CLI)
102
+ SENSOR_REGISTRY = {
103
+ # Named sensors (no I2C address needed)
104
+ "system": SystemSensor,
105
+ "gps": GPSSensor,
106
+ # I2C sensors
107
+ "mpu6050": MPU6050,
108
+ "mpu9250": MPU9250,
109
+ "bme280": BME280,
110
+ "ina219": INA219,
111
+ "sht3x": SHT3x,
112
+ "bh1750": BH1750,
113
+ "vl53l0x": VL53L0X,
114
+ "ads1115": ADS1115,
115
+ "qmc5883l": QMC5883L,
116
+ "hmc5883l": HMC5883L,
117
+ "adxl345": ADXL345,
118
+ }
@@ -0,0 +1,164 @@
1
+ """
2
+ ADS1115 16-bit ADC sensor driver.
3
+
4
+ The ADS1115 is a precision 16-bit ADC with 4 single-ended or 2 differential
5
+ channels. Programmable gain amplifier (PGA) and data rate.
6
+ Communicates via I2C at address 0x48–0x4B (configurable via ADDR pin).
7
+
8
+ Usage:
9
+ from plexus.sensors import ADS1115
10
+
11
+ sensor = ADS1115()
12
+ for reading in sensor.read():
13
+ print(f"{reading.metric}: {reading.value}")
14
+ """
15
+
16
+ import time
17
+ from typing import List, Optional
18
+ from .base import BaseSensor, SensorReading
19
+
20
+ ADS1115_ADDR = 0x48
21
+
22
+ # Register addresses
23
+ REG_CONVERSION = 0x00
24
+ REG_CONFIG = 0x01
25
+
26
+ # Config register bits
27
+ # OS: Start single conversion
28
+ OS_SINGLE = 0x8000
29
+ # MUX: Input multiplexer
30
+ MUX_AIN0 = 0x4000 # AIN0 vs GND
31
+ MUX_AIN1 = 0x5000 # AIN1 vs GND
32
+ MUX_AIN2 = 0x6000 # AIN2 vs GND
33
+ MUX_AIN3 = 0x7000 # AIN3 vs GND
34
+ # PGA: Programmable gain (full-scale voltage)
35
+ PGA_6144 = 0x0000 # ±6.144V (LSB = 187.5µV)
36
+ PGA_4096 = 0x0200 # ±4.096V (LSB = 125µV)
37
+ PGA_2048 = 0x0400 # ±2.048V (LSB = 62.5µV) — default
38
+ PGA_1024 = 0x0600 # ±1.024V
39
+ PGA_0512 = 0x0800 # ±0.512V
40
+ PGA_0256 = 0x0A00 # ±0.256V
41
+ # MODE: Operating mode
42
+ MODE_SINGLE = 0x0100 # Single-shot
43
+ # DR: Data rate
44
+ DR_128SPS = 0x0080 # 128 samples per second
45
+ # COMP: Disable comparator
46
+ COMP_DISABLE = 0x0003
47
+
48
+ MUX_CHANNELS = [MUX_AIN0, MUX_AIN1, MUX_AIN2, MUX_AIN3]
49
+
50
+ # Voltage per LSB for each PGA setting
51
+ PGA_LSB = {
52
+ PGA_6144: 0.0001875,
53
+ PGA_4096: 0.000125,
54
+ PGA_2048: 0.0000625,
55
+ PGA_1024: 0.00003125,
56
+ PGA_0512: 0.000015625,
57
+ PGA_0256: 0.0000078125,
58
+ }
59
+
60
+
61
+ class ADS1115(BaseSensor):
62
+ """
63
+ ADS1115 16-bit ADC driver.
64
+
65
+ Provides:
66
+ - adc_ch0 through adc_ch3: Voltage readings in volts (single-ended)
67
+
68
+ Default gain: ±4.096V (suitable for 3.3V/5V systems)
69
+ """
70
+
71
+ name = "ADS1115"
72
+ description = "16-bit ADC (4 channels, programmable gain)"
73
+ metrics = ["adc_ch0", "adc_ch1", "adc_ch2", "adc_ch3"]
74
+ i2c_addresses = [0x48, 0x49, 0x4A, 0x4B]
75
+
76
+ def __init__(
77
+ self,
78
+ address: int = ADS1115_ADDR,
79
+ bus: int = 1,
80
+ gain: int = PGA_4096,
81
+ channels: Optional[List[int]] = None,
82
+ sample_rate: float = 10.0,
83
+ prefix: str = "",
84
+ tags: Optional[dict] = None,
85
+ ):
86
+ super().__init__(sample_rate=sample_rate, prefix=prefix, tags=tags)
87
+ self.address = address
88
+ self.bus_num = bus
89
+ self.gain = gain
90
+ self.channels = channels if channels is not None else [0, 1, 2, 3]
91
+ self._bus = None
92
+ self._lsb = PGA_LSB.get(gain, 0.000125)
93
+
94
+ def setup(self) -> None:
95
+ try:
96
+ from smbus2 import SMBus
97
+ except ImportError:
98
+ raise ImportError(
99
+ "smbus2 is required for ADS1115. Install with: pip install smbus2"
100
+ )
101
+
102
+ self._bus = SMBus(self.bus_num)
103
+
104
+ def cleanup(self) -> None:
105
+ if self._bus:
106
+ self._bus.close()
107
+ self._bus = None
108
+
109
+ def _read_channel(self, channel: int) -> float:
110
+ """Read a single ADC channel and return voltage."""
111
+ config = (
112
+ OS_SINGLE |
113
+ MUX_CHANNELS[channel] |
114
+ self.gain |
115
+ MODE_SINGLE |
116
+ DR_128SPS |
117
+ COMP_DISABLE
118
+ )
119
+
120
+ # Write config to start conversion
121
+ high = (config >> 8) & 0xFF
122
+ low = config & 0xFF
123
+ self._bus.write_i2c_block_data(self.address, REG_CONFIG, [high, low])
124
+
125
+ # Wait for conversion (128 SPS = ~8ms per sample)
126
+ time.sleep(0.009)
127
+
128
+ # Read conversion result
129
+ data = self._bus.read_i2c_block_data(self.address, REG_CONVERSION, 2)
130
+ raw = (data[0] << 8) | data[1]
131
+
132
+ # Convert to signed
133
+ if raw > 32767:
134
+ raw -= 65536
135
+
136
+ return raw * self._lsb
137
+
138
+ def read(self) -> List[SensorReading]:
139
+ if self._bus is None:
140
+ self.setup()
141
+
142
+ readings = []
143
+ for ch in self.channels:
144
+ if 0 <= ch <= 3:
145
+ voltage = self._read_channel(ch)
146
+ readings.append(
147
+ SensorReading(f"adc_ch{ch}", round(voltage, 4))
148
+ )
149
+
150
+ return readings
151
+
152
+ def is_available(self) -> bool:
153
+ try:
154
+ from smbus2 import SMBus
155
+
156
+ bus = SMBus(self.bus_num)
157
+ # Read config register — default is 0x8583
158
+ data = bus.read_i2c_block_data(self.address, REG_CONFIG, 2)
159
+ bus.close()
160
+ config = (data[0] << 8) | data[1]
161
+ # Check OS bit is set (no conversion in progress)
162
+ return (config & 0x8000) != 0
163
+ except Exception:
164
+ return False
@@ -0,0 +1,179 @@
1
+ """
2
+ ADXL345 3-axis accelerometer driver.
3
+
4
+ Supports both I2C and SPI interfaces for the ADXL345 digital accelerometer.
5
+ Measures acceleration on X, Y, Z axes in units of g (9.81 m/s²).
6
+
7
+ Usage (I2C):
8
+ from plexus.sensors import ADXL345
9
+
10
+ accel = ADXL345(bus_type="i2c", address=0x53)
11
+ accel.setup()
12
+ for reading in accel.read():
13
+ print(f"{reading.metric}: {reading.value}")
14
+
15
+ Usage (SPI):
16
+ accel = ADXL345(bus_type="spi", spi_bus=0, spi_cs=0)
17
+ accel.setup()
18
+ for reading in accel.read():
19
+ print(f"{reading.metric}: {reading.value}")
20
+
21
+ With SensorHub:
22
+ from plexus.sensors import SensorHub, ADXL345
23
+ hub = SensorHub()
24
+ hub.add(ADXL345())
25
+ hub.run(Plexus())
26
+ """
27
+
28
+ import struct
29
+ import time
30
+ from typing import Dict, List, Optional
31
+
32
+ from .base import BaseSensor, SensorReading
33
+
34
+
35
+ # ADXL345 registers
36
+ _REG_DEVID = 0x00
37
+ _REG_BW_RATE = 0x2C
38
+ _REG_POWER_CTL = 0x2D
39
+ _REG_DATA_FORMAT = 0x31
40
+ _REG_DATAX0 = 0x32
41
+
42
+ # Expected chip ID
43
+ _CHIP_ID = 0xE5
44
+
45
+ # Scale factor: 4mg/LSB in full resolution mode
46
+ _SCALE_FACTOR = 0.004
47
+
48
+
49
+ class ADXL345(BaseSensor):
50
+ """ADXL345 3-axis accelerometer (I2C or SPI)."""
51
+
52
+ name = "ADXL345"
53
+ description = "3-axis accelerometer (±2g/±4g/±8g/±16g)"
54
+ metrics = ["accel_x", "accel_y", "accel_z"]
55
+ i2c_addresses = [0x53, 0x1D]
56
+ spi_devices = [(0, 0)]
57
+
58
+ def __init__(
59
+ self,
60
+ bus_type: str = "i2c",
61
+ address: int = 0x53,
62
+ bus: int = 1,
63
+ spi_bus: int = 0,
64
+ spi_cs: int = 0,
65
+ sample_rate: float = 100.0,
66
+ prefix: str = "",
67
+ tags: Optional[Dict[str, str]] = None,
68
+ ):
69
+ """
70
+ Args:
71
+ bus_type: "i2c" or "spi"
72
+ address: I2C address (0x53 or 0x1D)
73
+ bus: I2C bus number (usually 1 on Raspberry Pi)
74
+ spi_bus: SPI bus number
75
+ spi_cs: SPI chip select
76
+ sample_rate: Readings per second (Hz)
77
+ prefix: Prefix for metric names
78
+ tags: Tags to add to all readings
79
+ """
80
+ super().__init__(sample_rate=sample_rate, prefix=prefix, tags=tags)
81
+ self.bus_type = bus_type
82
+ self.address = address
83
+ self.bus_num = bus
84
+ self.spi_bus = spi_bus
85
+ self.spi_cs = spi_cs
86
+ self._dev = None
87
+
88
+ def _read_reg(self, reg: int, length: int = 1) -> bytes:
89
+ """Read register(s) via I2C or SPI."""
90
+ if self.bus_type == "spi":
91
+ # SPI read: bit 7 = read, bit 6 = multi-byte
92
+ cmd = reg | 0x80
93
+ if length > 1:
94
+ cmd |= 0x40
95
+ tx = [cmd] + [0x00] * length
96
+ rx = self._dev.xfer2(tx)
97
+ return bytes(rx[1:])
98
+ else:
99
+ if length == 1:
100
+ return bytes([self._dev.read_byte_data(self.address, reg)])
101
+ return bytes(self._dev.read_i2c_block_data(self.address, reg, length))
102
+
103
+ def _write_reg(self, reg: int, value: int):
104
+ """Write a register via I2C or SPI."""
105
+ if self.bus_type == "spi":
106
+ self._dev.xfer2([reg, value])
107
+ else:
108
+ self._dev.write_byte_data(self.address, reg, value)
109
+
110
+ def setup(self) -> None:
111
+ """Initialize the ADXL345."""
112
+ if self.bus_type == "spi":
113
+ import spidev
114
+ self._dev = spidev.SpiDev()
115
+ self._dev.open(self.spi_bus, self.spi_cs)
116
+ self._dev.mode = 3
117
+ self._dev.max_speed_hz = 1000000
118
+ else:
119
+ from smbus2 import SMBus
120
+ self._dev = SMBus(self.bus_num)
121
+
122
+ # Verify chip ID
123
+ chip_id = self._read_reg(_REG_DEVID)[0]
124
+ if chip_id != _CHIP_ID:
125
+ raise RuntimeError(
126
+ f"ADXL345 not found (got chip ID 0x{chip_id:02X}, expected 0x{_CHIP_ID:02X})"
127
+ )
128
+
129
+ # Configure: 100Hz output rate
130
+ self._write_reg(_REG_BW_RATE, 0x0A)
131
+ # Full resolution, ±2g range
132
+ self._write_reg(_REG_DATA_FORMAT, 0x08)
133
+ # Start measurement
134
+ self._write_reg(_REG_POWER_CTL, 0x08)
135
+
136
+ def cleanup(self) -> None:
137
+ """Clean up resources."""
138
+ if self._dev:
139
+ if self.bus_type == "spi":
140
+ self._dev.close()
141
+ else:
142
+ self._dev.close()
143
+ self._dev = None
144
+
145
+ def read(self) -> List[SensorReading]:
146
+ """Read acceleration on all three axes."""
147
+ if not self._dev:
148
+ return []
149
+
150
+ now = time.time()
151
+ raw = self._read_reg(_REG_DATAX0, 6)
152
+ x, y, z = struct.unpack("<hhh", raw)
153
+
154
+ return [
155
+ SensorReading("accel_x", round(x * _SCALE_FACTOR, 4), now),
156
+ SensorReading("accel_y", round(y * _SCALE_FACTOR, 4), now),
157
+ SensorReading("accel_z", round(z * _SCALE_FACTOR, 4), now),
158
+ ]
159
+
160
+ def is_available(self) -> bool:
161
+ """Check if ADXL345 is connected."""
162
+ try:
163
+ if self.bus_type == "spi":
164
+ import spidev
165
+ dev = spidev.SpiDev()
166
+ dev.open(self.spi_bus, self.spi_cs)
167
+ dev.mode = 3
168
+ dev.max_speed_hz = 1000000
169
+ resp = dev.xfer2([_REG_DEVID | 0x80, 0x00])
170
+ dev.close()
171
+ return resp[1] == _CHIP_ID
172
+ else:
173
+ from smbus2 import SMBus
174
+ bus = SMBus(self.bus_num)
175
+ chip_id = bus.read_byte_data(self.address, _REG_DEVID)
176
+ bus.close()
177
+ return chip_id == _CHIP_ID
178
+ except Exception:
179
+ return False