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/sensors/ina219.py
ADDED
|
@@ -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"]
|
plexus/sensors/sht3x.py
ADDED
|
@@ -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
|