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,149 @@
1
+ """
2
+ INA219 current/power monitor sensor driver.
3
+
4
+ The INA219 measures bus voltage, shunt voltage, current, and power.
5
+ Communicates via I2C at address 0x40–0x4F (configurable via A0/A1 pins).
6
+
7
+ Usage:
8
+ from plexus.sensors import INA219
9
+
10
+ sensor = INA219()
11
+ for reading in sensor.read():
12
+ print(f"{reading.metric}: {reading.value}")
13
+ """
14
+
15
+ from typing import List, Optional
16
+ from .base import BaseSensor, SensorReading
17
+
18
+ # Default I2C address
19
+ INA219_ADDR = 0x40
20
+
21
+ # Register addresses
22
+ REG_CONFIG = 0x00
23
+ REG_SHUNT_VOLTAGE = 0x01
24
+ REG_BUS_VOLTAGE = 0x02
25
+ REG_POWER = 0x03
26
+ REG_CURRENT = 0x04
27
+ REG_CALIBRATION = 0x05
28
+
29
+ # Configuration: 32V range, 320mV shunt range, 12-bit, continuous
30
+ CONFIG_DEFAULT = 0x399F
31
+
32
+ # Calibration for 0.1 ohm shunt resistor, max expected current 3.2A
33
+ # Cal = trunc(0.04096 / (current_lsb * r_shunt))
34
+ # current_lsb = max_expected / 2^15 = 3.2 / 32768 ≈ 0.0001
35
+ SHUNT_RESISTOR_OHMS = 0.1
36
+ CURRENT_LSB = 0.0001 # 100uA per bit
37
+ CAL_VALUE = int(0.04096 / (CURRENT_LSB * SHUNT_RESISTOR_OHMS))
38
+
39
+
40
+ class INA219(BaseSensor):
41
+ """
42
+ INA219 current/power monitor driver.
43
+
44
+ Provides:
45
+ - bus_voltage: Bus voltage in volts
46
+ - shunt_voltage: Shunt voltage in millivolts
47
+ - current_ma: Current in milliamps
48
+ - power_mw: Power in milliwatts
49
+ """
50
+
51
+ name = "INA219"
52
+ description = "Current/power monitor (voltage, current, power)"
53
+ metrics = ["bus_voltage", "shunt_voltage", "current_ma", "power_mw"]
54
+ i2c_addresses = [0x40, 0x41, 0x44, 0x45]
55
+
56
+ def __init__(
57
+ self,
58
+ address: int = INA219_ADDR,
59
+ bus: int = 1,
60
+ shunt_ohms: float = SHUNT_RESISTOR_OHMS,
61
+ max_current: float = 3.2,
62
+ sample_rate: float = 10.0,
63
+ prefix: str = "",
64
+ tags: Optional[dict] = None,
65
+ ):
66
+ super().__init__(sample_rate=sample_rate, prefix=prefix, tags=tags)
67
+ self.address = address
68
+ self.bus_num = bus
69
+ self.shunt_ohms = shunt_ohms
70
+ self.max_current = max_current
71
+ self._bus = None
72
+ self._current_lsb = max_current / 32768.0
73
+ self._cal_value = int(0.04096 / (self._current_lsb * shunt_ohms))
74
+
75
+ def setup(self) -> None:
76
+ try:
77
+ from smbus2 import SMBus
78
+ except ImportError:
79
+ raise ImportError(
80
+ "smbus2 is required for INA219. Install with: pip install smbus2"
81
+ )
82
+
83
+ self._bus = SMBus(self.bus_num)
84
+
85
+ # Write configuration
86
+ self._write_register(REG_CONFIG, CONFIG_DEFAULT)
87
+ # Write calibration
88
+ self._write_register(REG_CALIBRATION, self._cal_value)
89
+
90
+ def cleanup(self) -> None:
91
+ if self._bus:
92
+ self._bus.close()
93
+ self._bus = None
94
+
95
+ def _write_register(self, reg: int, value: int) -> None:
96
+ high = (value >> 8) & 0xFF
97
+ low = value & 0xFF
98
+ self._bus.write_i2c_block_data(self.address, reg, [high, low])
99
+
100
+ def _read_register(self, reg: int) -> int:
101
+ data = self._bus.read_i2c_block_data(self.address, reg, 2)
102
+ value = (data[0] << 8) | data[1]
103
+ return value
104
+
105
+ def _read_signed(self, reg: int) -> int:
106
+ value = self._read_register(reg)
107
+ if value > 32767:
108
+ value -= 65536
109
+ return value
110
+
111
+ def read(self) -> List[SensorReading]:
112
+ if self._bus is None:
113
+ self.setup()
114
+
115
+ # Bus voltage: bits [15:3] * 4mV, bit 1 = conversion ready
116
+ raw_bus = self._read_register(REG_BUS_VOLTAGE)
117
+ bus_voltage = (raw_bus >> 3) * 0.004 # 4mV per LSB
118
+
119
+ # Shunt voltage: signed 16-bit, 10uV per LSB
120
+ raw_shunt = self._read_signed(REG_SHUNT_VOLTAGE)
121
+ shunt_voltage_mv = raw_shunt * 0.01 # 10uV = 0.01mV
122
+
123
+ # Current: signed 16-bit * current_lsb
124
+ raw_current = self._read_signed(REG_CURRENT)
125
+ current_ma = raw_current * self._current_lsb * 1000.0
126
+
127
+ # Power: unsigned 16-bit * 20 * current_lsb
128
+ raw_power = self._read_register(REG_POWER)
129
+ power_mw = raw_power * 20.0 * self._current_lsb * 1000.0
130
+
131
+ return [
132
+ SensorReading("bus_voltage", round(bus_voltage, 3)),
133
+ SensorReading("shunt_voltage", round(shunt_voltage_mv, 3)),
134
+ SensorReading("current_ma", round(current_ma, 2)),
135
+ SensorReading("power_mw", round(power_mw, 2)),
136
+ ]
137
+
138
+ def is_available(self) -> bool:
139
+ try:
140
+ from smbus2 import SMBus
141
+
142
+ bus = SMBus(self.bus_num)
143
+ # Read config register — should return non-zero default
144
+ data = bus.read_i2c_block_data(self.address, REG_CONFIG, 2)
145
+ bus.close()
146
+ config = (data[0] << 8) | data[1]
147
+ return config != 0x0000 and config != 0xFFFF
148
+ except Exception:
149
+ return False
@@ -0,0 +1,239 @@
1
+ """
2
+ Magnetometer sensor drivers: QMC5883L and HMC5883L.
3
+
4
+ These sensors measure magnetic field strength in 3 axes.
5
+ Used for compass heading, metal detection, and orientation.
6
+
7
+ QMC5883L: Common Chinese replacement, address 0x0D
8
+ HMC5883L: Original Honeywell sensor, address 0x1E
9
+
10
+ Usage:
11
+ from plexus.sensors import QMC5883L, HMC5883L
12
+
13
+ mag = QMC5883L()
14
+ for reading in mag.read():
15
+ print(f"{reading.metric}: {reading.value}")
16
+ """
17
+
18
+ import time
19
+ from typing import List, Optional
20
+ from .base import BaseSensor, SensorReading
21
+
22
+ # ─── QMC5883L ────────────────────────────────────────────────────────────────
23
+
24
+ QMC5883L_ADDR = 0x0D
25
+
26
+ QMC_REG_DATA = 0x00 # X LSB, X MSB, Y LSB, Y MSB, Z LSB, Z MSB
27
+ QMC_REG_STATUS = 0x06
28
+ QMC_REG_CONFIG1 = 0x09 # Mode, ODR, Range, OSR
29
+ QMC_REG_CONFIG2 = 0x0A # Soft reset, pointer roll-over
30
+ QMC_REG_CHIP_ID = 0x0D
31
+
32
+ # Config1: Continuous mode, 200Hz ODR, 8 Gauss range, 512 oversampling
33
+ QMC_CONFIG1_DEFAULT = 0x1D
34
+ # Config2: Pointer roll-over enabled
35
+ QMC_CONFIG2_DEFAULT = 0x40
36
+
37
+
38
+ class QMC5883L(BaseSensor):
39
+ """
40
+ QMC5883L 3-axis magnetometer driver.
41
+
42
+ Provides:
43
+ - mag_x, mag_y, mag_z: Magnetic field in microtesla (µT)
44
+ """
45
+
46
+ name = "QMC5883L"
47
+ description = "3-axis magnetometer (compass)"
48
+ metrics = ["mag_x", "mag_y", "mag_z"]
49
+ i2c_addresses = [QMC5883L_ADDR]
50
+
51
+ def __init__(
52
+ self,
53
+ address: int = QMC5883L_ADDR,
54
+ bus: int = 1,
55
+ sample_rate: float = 10.0,
56
+ prefix: str = "",
57
+ tags: Optional[dict] = None,
58
+ ):
59
+ super().__init__(sample_rate=sample_rate, prefix=prefix, tags=tags)
60
+ self.address = address
61
+ self.bus_num = bus
62
+ self._bus = None
63
+
64
+ def setup(self) -> None:
65
+ try:
66
+ from smbus2 import SMBus
67
+ except ImportError:
68
+ raise ImportError(
69
+ "smbus2 is required for QMC5883L. Install with: pip install smbus2"
70
+ )
71
+
72
+ self._bus = SMBus(self.bus_num)
73
+
74
+ # Soft reset
75
+ self._bus.write_byte_data(self.address, QMC_REG_CONFIG2, 0x80)
76
+ time.sleep(0.01)
77
+
78
+ # Configure: continuous mode, 200Hz, 8G range, 512x oversampling
79
+ self._bus.write_byte_data(self.address, QMC_REG_CONFIG1, QMC_CONFIG1_DEFAULT)
80
+ self._bus.write_byte_data(self.address, QMC_REG_CONFIG2, QMC_CONFIG2_DEFAULT)
81
+ time.sleep(0.01)
82
+
83
+ def cleanup(self) -> None:
84
+ if self._bus:
85
+ self._bus.close()
86
+ self._bus = None
87
+
88
+ def read(self) -> List[SensorReading]:
89
+ if self._bus is None:
90
+ self.setup()
91
+
92
+ # Check data ready
93
+ status = self._bus.read_byte_data(self.address, QMC_REG_STATUS)
94
+ if not (status & 0x01):
95
+ return [] # Data not ready
96
+
97
+ # Read 6 bytes: X_LSB, X_MSB, Y_LSB, Y_MSB, Z_LSB, Z_MSB
98
+ data = self._bus.read_i2c_block_data(self.address, QMC_REG_DATA, 6)
99
+
100
+ x = (data[1] << 8) | data[0]
101
+ y = (data[3] << 8) | data[2]
102
+ z = (data[5] << 8) | data[4]
103
+
104
+ # Convert to signed
105
+ if x > 32767:
106
+ x -= 65536
107
+ if y > 32767:
108
+ y -= 65536
109
+ if z > 32767:
110
+ z -= 65536
111
+
112
+ # At 8 Gauss range: 3000 LSB/Gauss, 1 Gauss = 100 µT
113
+ # So LSB = 100/3000 µT ≈ 0.0333 µT
114
+ scale = 100.0 / 3000.0
115
+
116
+ return [
117
+ SensorReading("mag_x", round(x * scale, 2)),
118
+ SensorReading("mag_y", round(y * scale, 2)),
119
+ SensorReading("mag_z", round(z * scale, 2)),
120
+ ]
121
+
122
+ def is_available(self) -> bool:
123
+ try:
124
+ from smbus2 import SMBus
125
+
126
+ bus = SMBus(self.bus_num)
127
+ chip_id = bus.read_byte_data(self.address, QMC_REG_CHIP_ID)
128
+ bus.close()
129
+ return chip_id == 0xFF # QMC5883L returns 0xFF for chip ID register
130
+ except Exception:
131
+ return False
132
+
133
+
134
+ # ─── HMC5883L ────────────────────────────────────────────────────────────────
135
+
136
+ HMC5883L_ADDR = 0x1E
137
+
138
+ HMC_REG_CONFIG_A = 0x00
139
+ HMC_REG_CONFIG_B = 0x01
140
+ HMC_REG_MODE = 0x02
141
+ HMC_REG_DATA = 0x03 # X MSB, X LSB, Z MSB, Z LSB, Y MSB, Y LSB
142
+ HMC_REG_ID_A = 0x0A
143
+
144
+ # Config A: 8 samples averaged, 15Hz output, normal measurement
145
+ HMC_CONFIG_A_DEFAULT = 0x70
146
+ # Config B: Gain = 1.3 Gauss (1090 LSB/Gauss)
147
+ HMC_CONFIG_B_DEFAULT = 0x20
148
+ # Mode: Continuous measurement
149
+ HMC_MODE_CONTINUOUS = 0x00
150
+
151
+
152
+ class HMC5883L(BaseSensor):
153
+ """
154
+ HMC5883L 3-axis magnetometer driver.
155
+
156
+ Provides:
157
+ - mag_x, mag_y, mag_z: Magnetic field in microtesla (µT)
158
+ """
159
+
160
+ name = "HMC5883L"
161
+ description = "3-axis magnetometer (compass)"
162
+ metrics = ["mag_x", "mag_y", "mag_z"]
163
+ i2c_addresses = [HMC5883L_ADDR]
164
+
165
+ def __init__(
166
+ self,
167
+ address: int = HMC5883L_ADDR,
168
+ bus: int = 1,
169
+ sample_rate: float = 10.0,
170
+ prefix: str = "",
171
+ tags: Optional[dict] = None,
172
+ ):
173
+ super().__init__(sample_rate=sample_rate, prefix=prefix, tags=tags)
174
+ self.address = address
175
+ self.bus_num = bus
176
+ self._bus = None
177
+
178
+ def setup(self) -> None:
179
+ try:
180
+ from smbus2 import SMBus
181
+ except ImportError:
182
+ raise ImportError(
183
+ "smbus2 is required for HMC5883L. Install with: pip install smbus2"
184
+ )
185
+
186
+ self._bus = SMBus(self.bus_num)
187
+
188
+ self._bus.write_byte_data(self.address, HMC_REG_CONFIG_A, HMC_CONFIG_A_DEFAULT)
189
+ self._bus.write_byte_data(self.address, HMC_REG_CONFIG_B, HMC_CONFIG_B_DEFAULT)
190
+ self._bus.write_byte_data(self.address, HMC_REG_MODE, HMC_MODE_CONTINUOUS)
191
+ time.sleep(0.01)
192
+
193
+ def cleanup(self) -> None:
194
+ if self._bus:
195
+ self._bus.close()
196
+ self._bus = None
197
+
198
+ def read(self) -> List[SensorReading]:
199
+ if self._bus is None:
200
+ self.setup()
201
+
202
+ # Read 6 bytes: X MSB, X LSB, Z MSB, Z LSB, Y MSB, Y LSB
203
+ # Note: HMC5883L register order is X, Z, Y (not X, Y, Z)
204
+ data = self._bus.read_i2c_block_data(self.address, HMC_REG_DATA, 6)
205
+
206
+ x = (data[0] << 8) | data[1]
207
+ z = (data[2] << 8) | data[3]
208
+ y = (data[4] << 8) | data[5]
209
+
210
+ # Convert to signed
211
+ if x > 32767:
212
+ x -= 65536
213
+ if y > 32767:
214
+ y -= 65536
215
+ if z > 32767:
216
+ z -= 65536
217
+
218
+ # At 1.3 Gauss gain: 1090 LSB/Gauss, 1 Gauss = 100 µT
219
+ scale = 100.0 / 1090.0
220
+
221
+ return [
222
+ SensorReading("mag_x", round(x * scale, 2)),
223
+ SensorReading("mag_y", round(y * scale, 2)),
224
+ SensorReading("mag_z", round(z * scale, 2)),
225
+ ]
226
+
227
+ def is_available(self) -> bool:
228
+ try:
229
+ from smbus2 import SMBus
230
+
231
+ bus = SMBus(self.bus_num)
232
+ # Read identification registers (should be 'H', '4', '3')
233
+ id_a = bus.read_byte_data(self.address, HMC_REG_ID_A)
234
+ id_b = bus.read_byte_data(self.address, HMC_REG_ID_A + 1)
235
+ id_c = bus.read_byte_data(self.address, HMC_REG_ID_A + 2)
236
+ bus.close()
237
+ return id_a == 0x48 and id_b == 0x34 and id_c == 0x33 # "H43"
238
+ except Exception:
239
+ return False
@@ -0,0 +1,162 @@
1
+ """
2
+ MPU6050 6-axis IMU sensor driver.
3
+
4
+ The MPU6050 provides 3-axis accelerometer and 3-axis gyroscope data.
5
+ Communicates via I2C at address 0x68 (or 0x69 if AD0 pin is high).
6
+
7
+ Usage:
8
+ from plexus import Plexus
9
+ from plexus.sensors import MPU6050
10
+
11
+ px = Plexus()
12
+ imu = MPU6050()
13
+
14
+ while True:
15
+ for reading in imu.read():
16
+ px.send(reading.metric, reading.value)
17
+ time.sleep(0.01) # 100 Hz
18
+
19
+ Or with SensorHub:
20
+ from plexus.sensors import SensorHub, MPU6050
21
+
22
+ hub = SensorHub()
23
+ hub.add(MPU6050(sample_rate=100))
24
+ hub.run(Plexus())
25
+ """
26
+
27
+ from typing import List, Optional
28
+ from .base import BaseSensor, SensorReading
29
+
30
+ # I2C constants
31
+ MPU6050_ADDR = 0x68
32
+ MPU6050_ADDR_ALT = 0x69
33
+
34
+ # Register addresses
35
+ PWR_MGMT_1 = 0x6B
36
+ ACCEL_XOUT_H = 0x3B
37
+ GYRO_XOUT_H = 0x43
38
+ WHO_AM_I = 0x75
39
+
40
+ # Scale factors for default ranges
41
+ ACCEL_SCALE_2G = 16384.0 # LSB/g for ±2g
42
+ GYRO_SCALE_250 = 131.0 # LSB/(°/s) for ±250°/s
43
+
44
+
45
+ class MPU6050(BaseSensor):
46
+ """
47
+ MPU6050 6-axis IMU sensor driver.
48
+
49
+ Provides:
50
+ - accel_x, accel_y, accel_z: Acceleration in g (±2g range)
51
+ - gyro_x, gyro_y, gyro_z: Angular velocity in °/s (±250°/s range)
52
+ """
53
+
54
+ name = "MPU6050"
55
+ description = "6-axis IMU (accelerometer + gyroscope)"
56
+ metrics = ["accel_x", "accel_y", "accel_z", "gyro_x", "gyro_y", "gyro_z"]
57
+ i2c_addresses = [MPU6050_ADDR, MPU6050_ADDR_ALT]
58
+
59
+ def __init__(
60
+ self,
61
+ address: int = MPU6050_ADDR,
62
+ bus: int = 1,
63
+ sample_rate: float = 100.0,
64
+ prefix: str = "",
65
+ tags: Optional[dict] = None,
66
+ ):
67
+ """
68
+ Initialize MPU6050 driver.
69
+
70
+ Args:
71
+ address: I2C address (0x68 or 0x69)
72
+ bus: I2C bus number (usually 1 on Raspberry Pi)
73
+ sample_rate: Readings per second
74
+ prefix: Prefix for metric names
75
+ tags: Tags to add to all readings
76
+ """
77
+ super().__init__(sample_rate=sample_rate, prefix=prefix, tags=tags)
78
+ self.address = address
79
+ self.bus_num = bus
80
+ self._bus = None
81
+
82
+ def setup(self) -> None:
83
+ """Initialize the MPU6050."""
84
+ try:
85
+ from smbus2 import SMBus
86
+ except ImportError:
87
+ raise ImportError(
88
+ "smbus2 is required for MPU6050. Install with: pip install smbus2"
89
+ )
90
+
91
+ self._bus = SMBus(self.bus_num)
92
+
93
+ # Wake up the sensor (clear sleep bit)
94
+ self._bus.write_byte_data(self.address, PWR_MGMT_1, 0x00)
95
+
96
+ def cleanup(self) -> None:
97
+ """Close I2C bus."""
98
+ if self._bus:
99
+ self._bus.close()
100
+ self._bus = None
101
+
102
+ def _read_raw(self, register: int) -> int:
103
+ """Read 16-bit signed value from register pair."""
104
+ high = self._bus.read_byte_data(self.address, register)
105
+ low = self._bus.read_byte_data(self.address, register + 1)
106
+ value = (high << 8) | low
107
+ if value > 32767:
108
+ value -= 65536
109
+ return value
110
+
111
+ def read(self) -> List[SensorReading]:
112
+ """Read accelerometer and gyroscope data."""
113
+ if self._bus is None:
114
+ self.setup()
115
+
116
+ # Read accelerometer (g)
117
+ accel_x = self._read_raw(ACCEL_XOUT_H) / ACCEL_SCALE_2G
118
+ accel_y = self._read_raw(ACCEL_XOUT_H + 2) / ACCEL_SCALE_2G
119
+ accel_z = self._read_raw(ACCEL_XOUT_H + 4) / ACCEL_SCALE_2G
120
+
121
+ # Read gyroscope (°/s)
122
+ gyro_x = self._read_raw(GYRO_XOUT_H) / GYRO_SCALE_250
123
+ gyro_y = self._read_raw(GYRO_XOUT_H + 2) / GYRO_SCALE_250
124
+ gyro_z = self._read_raw(GYRO_XOUT_H + 4) / GYRO_SCALE_250
125
+
126
+ return [
127
+ SensorReading("accel_x", round(accel_x, 4)),
128
+ SensorReading("accel_y", round(accel_y, 4)),
129
+ SensorReading("accel_z", round(accel_z, 4)),
130
+ SensorReading("gyro_x", round(gyro_x, 2)),
131
+ SensorReading("gyro_y", round(gyro_y, 2)),
132
+ SensorReading("gyro_z", round(gyro_z, 2)),
133
+ ]
134
+
135
+ def is_available(self) -> bool:
136
+ """Check if MPU6050 is connected."""
137
+ try:
138
+ from smbus2 import SMBus
139
+
140
+ bus = SMBus(self.bus_num)
141
+ who_am_i = bus.read_byte_data(self.address, WHO_AM_I)
142
+ bus.close()
143
+ # Accept various MPU variants:
144
+ # 0x68 = MPU6050, 0x70 = MPU6500, 0x71 = MPU9250, 0x73 = MPU9255
145
+ return who_am_i in (0x68, 0x70, 0x71, 0x73)
146
+ except Exception:
147
+ return False
148
+
149
+
150
+ class MPU9250(MPU6050):
151
+ """
152
+ MPU9250 IMU sensor driver.
153
+
154
+ The MPU9250 contains an MPU6500 (accel + gyro) and an AK8963 magnetometer
155
+ on an auxiliary I2C bus. This driver currently reads the 6-axis accel/gyro
156
+ data. Magnetometer support requires enabling the AK8963 pass-through mode
157
+ and is not yet implemented.
158
+ """
159
+
160
+ name = "MPU9250"
161
+ description = "6-axis IMU (accelerometer + gyroscope) — magnetometer not yet supported"
162
+ metrics = ["accel_x", "accel_y", "accel_z", "gyro_x", "gyro_y", "gyro_z"]
@@ -0,0 +1,139 @@
1
+ """
2
+ SHT3x precision temperature and humidity sensor driver.
3
+
4
+ The SHT31/SHT35 provides high-accuracy temperature (±0.2°C) and
5
+ humidity (±2%) readings. Communicates via I2C at address 0x44 or 0x45.
6
+
7
+ Usage:
8
+ from plexus.sensors import SHT3x
9
+
10
+ sensor = SHT3x()
11
+ for reading in sensor.read():
12
+ print(f"{reading.metric}: {reading.value}")
13
+ """
14
+
15
+ import time
16
+ from typing import List, Optional
17
+ from .base import BaseSensor, SensorReading
18
+
19
+ SHT3X_ADDR = 0x44
20
+ SHT3X_ADDR_ALT = 0x45
21
+
22
+ # Single-shot measurement commands (clock stretching disabled)
23
+ CMD_MEAS_HIGH = [0x24, 0x00] # High repeatability
24
+ CMD_MEAS_MEDIUM = [0x24, 0x0B] # Medium repeatability
25
+ CMD_MEAS_LOW = [0x24, 0x16] # Low repeatability
26
+
27
+ # Status register
28
+ CMD_STATUS = [0xF3, 0x2D]
29
+
30
+ # Soft reset
31
+ CMD_RESET = [0x30, 0xA2]
32
+
33
+
34
+ def _crc8(data: bytes) -> int:
35
+ """CRC-8 check per SHT3x datasheet (polynomial 0x31)."""
36
+ crc = 0xFF
37
+ for byte in data:
38
+ crc ^= byte
39
+ for _ in range(8):
40
+ if crc & 0x80:
41
+ crc = (crc << 1) ^ 0x31
42
+ else:
43
+ crc = crc << 1
44
+ crc &= 0xFF
45
+ return crc
46
+
47
+
48
+ class SHT3x(BaseSensor):
49
+ """
50
+ SHT3x (SHT31/SHT35) precision humidity and temperature sensor.
51
+
52
+ Provides:
53
+ - sht_temperature: Temperature in °C (±0.2°C accuracy)
54
+ - sht_humidity: Relative humidity in % (±2% accuracy)
55
+ """
56
+
57
+ name = "SHT3x"
58
+ description = "Precision temperature and humidity (±0.2°C, ±2% RH)"
59
+ metrics = ["sht_temperature", "sht_humidity"]
60
+ i2c_addresses = [SHT3X_ADDR, SHT3X_ADDR_ALT]
61
+
62
+ def __init__(
63
+ self,
64
+ address: int = SHT3X_ADDR,
65
+ bus: int = 1,
66
+ sample_rate: float = 1.0,
67
+ prefix: str = "",
68
+ tags: Optional[dict] = None,
69
+ ):
70
+ super().__init__(sample_rate=sample_rate, prefix=prefix, tags=tags)
71
+ self.address = address
72
+ self.bus_num = bus
73
+ self._bus = None
74
+
75
+ def setup(self) -> None:
76
+ try:
77
+ from smbus2 import SMBus
78
+ except ImportError:
79
+ raise ImportError(
80
+ "smbus2 is required for SHT3x. Install with: pip install smbus2"
81
+ )
82
+
83
+ self._bus = SMBus(self.bus_num)
84
+
85
+ # Soft reset
86
+ self._bus.write_i2c_block_data(self.address, CMD_RESET[0], [CMD_RESET[1]])
87
+ time.sleep(0.002) # 1.5ms reset time
88
+
89
+ def cleanup(self) -> None:
90
+ if self._bus:
91
+ self._bus.close()
92
+ self._bus = None
93
+
94
+ def read(self) -> List[SensorReading]:
95
+ if self._bus is None:
96
+ self.setup()
97
+
98
+ # Trigger single-shot measurement (high repeatability)
99
+ self._bus.write_i2c_block_data(self.address, CMD_MEAS_HIGH[0], [CMD_MEAS_HIGH[1]])
100
+ time.sleep(0.016) # 15.5ms max for high repeatability
101
+
102
+ # Read 6 bytes: temp_msb, temp_lsb, temp_crc, hum_msb, hum_lsb, hum_crc
103
+ data = self._bus.read_i2c_block_data(self.address, 0x00, 6)
104
+
105
+ # Verify CRCs
106
+ if _crc8(bytes(data[0:2])) != data[2]:
107
+ return [] # Temperature CRC mismatch
108
+ if _crc8(bytes(data[3:5])) != data[5]:
109
+ return [] # Humidity CRC mismatch
110
+
111
+ # Convert raw values
112
+ raw_temp = (data[0] << 8) | data[1]
113
+ raw_hum = (data[3] << 8) | data[4]
114
+
115
+ temperature = -45.0 + 175.0 * (raw_temp / 65535.0)
116
+ humidity = 100.0 * (raw_hum / 65535.0)
117
+
118
+ # Clamp humidity to valid range
119
+ humidity = max(0.0, min(100.0, humidity))
120
+
121
+ return [
122
+ SensorReading("sht_temperature", round(temperature, 2)),
123
+ SensorReading("sht_humidity", round(humidity, 1)),
124
+ ]
125
+
126
+ def is_available(self) -> bool:
127
+ try:
128
+ from smbus2 import SMBus
129
+
130
+ bus = SMBus(self.bus_num)
131
+ # Read status register
132
+ bus.write_i2c_block_data(self.address, CMD_STATUS[0], [CMD_STATUS[1]])
133
+ time.sleep(0.001)
134
+ data = bus.read_i2c_block_data(self.address, 0x00, 3)
135
+ bus.close()
136
+ # Check CRC of status
137
+ return _crc8(bytes(data[0:2])) == data[2]
138
+ except Exception:
139
+ return False