You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Fix unit tests
This commit is contained in:
@@ -449,6 +449,8 @@ Current stable version: 0.3.3 (as of latest CHANGELOG entry)
|
||||
- Config/preferences: `internal_filesystem/lib/mpos/config.py`
|
||||
- Top menu/drawer: `internal_filesystem/lib/mpos/ui/topmenu.py`
|
||||
- Activity navigation: `internal_filesystem/lib/mpos/activity_navigator.py`
|
||||
- Sensor management: `internal_filesystem/lib/mpos/sensor_manager.py`
|
||||
- IMU drivers: `internal_filesystem/lib/mpos/hardware/drivers/qmi8658.py` and `wsen_isds.py`
|
||||
|
||||
## Common Utilities and Helpers
|
||||
|
||||
@@ -642,6 +644,7 @@ def defocus_handler(self, obj):
|
||||
- `mpos.sdcard.SDCardManager`: SD card mounting and management
|
||||
- `mpos.clipboard`: System clipboard access
|
||||
- `mpos.battery_voltage`: Battery level reading (ESP32 only)
|
||||
- `mpos.sensor_manager`: Unified sensor access (accelerometer, gyroscope, temperature)
|
||||
|
||||
## Audio System (AudioFlinger)
|
||||
|
||||
@@ -849,6 +852,173 @@ class LEDAnimationActivity(Activity):
|
||||
- **Thread-safe**: No locking (single-threaded usage recommended)
|
||||
- **Desktop**: Functions return `False` (no-op) on desktop builds
|
||||
|
||||
## Sensor System (SensorManager)
|
||||
|
||||
MicroPythonOS provides a unified sensor framework called **SensorManager** (Android-inspired) that provides easy access to motion sensors (accelerometer, gyroscope) and temperature sensors across different hardware platforms.
|
||||
|
||||
### Supported Sensors
|
||||
|
||||
**IMU Sensors:**
|
||||
- **QMI8658** (Waveshare ESP32-S3): Accelerometer, Gyroscope, Temperature
|
||||
- **WSEN_ISDS** (Fri3d Camp 2024 Badge): Accelerometer, Gyroscope
|
||||
|
||||
**Temperature Sensors:**
|
||||
- **ESP32 MCU Temperature**: Internal SoC temperature sensor
|
||||
- **IMU Chip Temperature**: QMI8658 chip temperature
|
||||
|
||||
### Basic Usage
|
||||
|
||||
**Check availability and read sensors**:
|
||||
```python
|
||||
import mpos.sensor_manager as SensorManager
|
||||
|
||||
# Check if sensors are available
|
||||
if SensorManager.is_available():
|
||||
# Get sensors
|
||||
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
|
||||
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
|
||||
temp = SensorManager.get_default_sensor(SensorManager.TYPE_SOC_TEMPERATURE)
|
||||
|
||||
# Read data (returns standard SI units)
|
||||
accel_data = SensorManager.read_sensor(accel) # Returns (x, y, z) in m/s²
|
||||
gyro_data = SensorManager.read_sensor(gyro) # Returns (x, y, z) in deg/s
|
||||
temperature = SensorManager.read_sensor(temp) # Returns °C
|
||||
|
||||
if accel_data:
|
||||
ax, ay, az = accel_data
|
||||
print(f"Acceleration: {ax:.2f}, {ay:.2f}, {az:.2f} m/s²")
|
||||
```
|
||||
|
||||
### Sensor Types
|
||||
|
||||
```python
|
||||
# Motion sensors
|
||||
SensorManager.TYPE_ACCELEROMETER # m/s² (meters per second squared)
|
||||
SensorManager.TYPE_GYROSCOPE # deg/s (degrees per second)
|
||||
|
||||
# Temperature sensors
|
||||
SensorManager.TYPE_SOC_TEMPERATURE # °C (MCU internal temperature)
|
||||
SensorManager.TYPE_IMU_TEMPERATURE # °C (IMU chip temperature)
|
||||
```
|
||||
|
||||
### Tilt-Controlled Game Example
|
||||
|
||||
```python
|
||||
from mpos.app.activity import Activity
|
||||
import mpos.sensor_manager as SensorManager
|
||||
import mpos.ui
|
||||
import time
|
||||
|
||||
class TiltBallActivity(Activity):
|
||||
def onCreate(self):
|
||||
self.screen = lv.obj()
|
||||
|
||||
# Get accelerometer
|
||||
self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
|
||||
|
||||
# Create ball UI
|
||||
self.ball = lv.obj(self.screen)
|
||||
self.ball.set_size(20, 20)
|
||||
self.ball.set_style_radius(10, 0)
|
||||
|
||||
# Physics state
|
||||
self.ball_x = 160.0
|
||||
self.ball_y = 120.0
|
||||
self.ball_vx = 0.0
|
||||
self.ball_vy = 0.0
|
||||
self.last_time = time.ticks_ms()
|
||||
|
||||
self.setContentView(self.screen)
|
||||
|
||||
def onResume(self, screen):
|
||||
self.last_time = time.ticks_ms()
|
||||
mpos.ui.task_handler.add_event_cb(self.update_physics, 1)
|
||||
|
||||
def onPause(self, screen):
|
||||
mpos.ui.task_handler.remove_event_cb(self.update_physics)
|
||||
|
||||
def update_physics(self, a, b):
|
||||
current_time = time.ticks_ms()
|
||||
delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0
|
||||
self.last_time = current_time
|
||||
|
||||
# Read accelerometer
|
||||
accel = SensorManager.read_sensor(self.accel)
|
||||
if accel:
|
||||
ax, ay, az = accel
|
||||
|
||||
# Apply acceleration to velocity
|
||||
self.ball_vx += (ax * 5.0) * delta_time
|
||||
self.ball_vy -= (ay * 5.0) * delta_time # Flip Y
|
||||
|
||||
# Update position
|
||||
self.ball_x += self.ball_vx
|
||||
self.ball_y += self.ball_vy
|
||||
|
||||
# Update ball position
|
||||
self.ball.set_pos(int(self.ball_x), int(self.ball_y))
|
||||
```
|
||||
|
||||
### Calibration
|
||||
|
||||
Calibration removes sensor drift and improves accuracy. The device must be **stationary** during calibration.
|
||||
|
||||
```python
|
||||
# Calibrate accelerometer and gyroscope
|
||||
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
|
||||
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
|
||||
|
||||
# Calibrate (100 samples, device must be flat and still)
|
||||
accel_offsets = SensorManager.calibrate_sensor(accel, samples=100)
|
||||
gyro_offsets = SensorManager.calibrate_sensor(gyro, samples=100)
|
||||
|
||||
# Calibration is automatically saved to SharedPreferences
|
||||
# and loaded on next boot
|
||||
```
|
||||
|
||||
### Performance Recommendations
|
||||
|
||||
**Polling rate recommendations:**
|
||||
- **Games**: 20-30 Hz (responsive but not excessive)
|
||||
- **UI feedback**: 10-15 Hz (smooth for tilt UI)
|
||||
- **Background monitoring**: 1-5 Hz (screen rotation, pedometer)
|
||||
|
||||
```python
|
||||
# ❌ BAD: Poll every frame (60 Hz)
|
||||
def update_frame(self, a, b):
|
||||
accel = SensorManager.read_sensor(self.accel) # Too frequent!
|
||||
|
||||
# ✅ GOOD: Poll every other frame (30 Hz)
|
||||
def update_frame(self, a, b):
|
||||
self.frame_count += 1
|
||||
if self.frame_count % 2 == 0:
|
||||
accel = SensorManager.read_sensor(self.accel)
|
||||
```
|
||||
|
||||
### Hardware Support Matrix
|
||||
|
||||
| Platform | Accelerometer | Gyroscope | IMU Temp | MCU Temp |
|
||||
|----------|---------------|-----------|----------|----------|
|
||||
| Waveshare ESP32-S3 | ✅ QMI8658 | ✅ QMI8658 | ✅ QMI8658 | ✅ ESP32 |
|
||||
| Fri3d 2024 Badge | ✅ WSEN_ISDS | ✅ WSEN_ISDS | ❌ | ✅ ESP32 |
|
||||
| Desktop/Linux | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
### Implementation Details
|
||||
|
||||
- **Location**: `lib/mpos/sensor_manager.py`
|
||||
- **Pattern**: Module-level singleton (similar to `battery_voltage.py`)
|
||||
- **Units**: Standard SI (m/s² for acceleration, deg/s for gyroscope, °C for temperature)
|
||||
- **Calibration**: Persistent via SharedPreferences (`data/com.micropythonos.sensors/config.json`)
|
||||
- **Thread-safe**: Uses locks for concurrent access
|
||||
- **Auto-detection**: Identifies IMU type via chip ID registers
|
||||
- **Desktop**: Functions return `None` (graceful fallback) on desktop builds
|
||||
|
||||
### Driver Locations
|
||||
|
||||
- **QMI8658**: `lib/mpos/hardware/drivers/qmi8658.py`
|
||||
- **WSEN_ISDS**: `lib/mpos/hardware/drivers/wsen_isds.py`
|
||||
- **Board init**: `lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py` and `lib/mpos/board/fri3d_2024.py`
|
||||
|
||||
## Animations and Game Loops
|
||||
|
||||
MicroPythonOS supports frame-based animations and game loops using the TaskHandler event system. This pattern is used for games, particle effects, and smooth animations.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from mpos.apps import Activity
|
||||
import mpos.sensor_manager as SensorManager
|
||||
|
||||
class IMU(Activity):
|
||||
|
||||
sensor = None
|
||||
accel_sensor = None
|
||||
gyro_sensor = None
|
||||
temp_sensor = None
|
||||
refresh_timer = None
|
||||
|
||||
# widgets:
|
||||
@@ -30,12 +33,16 @@ class IMU(Activity):
|
||||
self.slidergz = lv.slider(screen)
|
||||
self.slidergz.align(lv.ALIGN.CENTER, 0, 90)
|
||||
try:
|
||||
from machine import Pin, I2C
|
||||
from qmi8658 import QMI8658
|
||||
import machine
|
||||
self.sensor = QMI8658(I2C(0, sda=machine.Pin(48), scl=machine.Pin(47)))
|
||||
print("IMU sensor initialized")
|
||||
#print(f"{self.sensor.temperature=} {self.sensor.acceleration=} {self.sensor.gyro=}")
|
||||
if SensorManager.is_available():
|
||||
self.accel_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
|
||||
self.gyro_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
|
||||
# Get IMU temperature (not MCU temperature)
|
||||
self.temp_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_IMU_TEMPERATURE)
|
||||
print("IMU sensors initialized via SensorManager")
|
||||
print(f"Available sensors: {SensorManager.get_sensor_list()}")
|
||||
else:
|
||||
print("Warning: No IMU sensors available")
|
||||
self.templabel.set_text("No IMU sensors available")
|
||||
except Exception as e:
|
||||
warning = f"Warning: could not initialize IMU hardware:\n{e}"
|
||||
print(warning)
|
||||
@@ -68,22 +75,45 @@ class IMU(Activity):
|
||||
|
||||
def refresh(self, timer):
|
||||
#print("refresh timer")
|
||||
if self.sensor:
|
||||
#print(f"{self.sensor.temperature=} {self.sensor.acceleration=} {self.sensor.gyro=}")
|
||||
temp = self.sensor.temperature
|
||||
ax = self.sensor.acceleration[0]
|
||||
axp = int((ax * 100 + 100)/2)
|
||||
ay = self.sensor.acceleration[1]
|
||||
ayp = int((ay * 100 + 100)/2)
|
||||
az = self.sensor.acceleration[2]
|
||||
azp = int((az * 100 + 100)/2)
|
||||
# values between -200 and 200 => /4 becomes -50 and 50 => +50 becomes 0 and 100
|
||||
gx = self.convert_percentage(self.sensor.gyro[0])
|
||||
gy = self.convert_percentage(self.sensor.gyro[1])
|
||||
gz = self.convert_percentage(self.sensor.gyro[2])
|
||||
self.templabel.set_text(f"IMU chip temperature: {temp:.2f}°C")
|
||||
if self.accel_sensor and self.gyro_sensor:
|
||||
# Read sensor data via SensorManager (returns m/s² for accel, deg/s for gyro)
|
||||
accel = SensorManager.read_sensor(self.accel_sensor)
|
||||
gyro = SensorManager.read_sensor(self.gyro_sensor)
|
||||
temp = SensorManager.read_sensor(self.temp_sensor) if self.temp_sensor else None
|
||||
|
||||
if accel and gyro:
|
||||
# Convert m/s² to G for display (divide by 9.80665)
|
||||
# Range: ±8G → ±1G = ±10% of range → map to 0-100
|
||||
ax, ay, az = accel
|
||||
ax_g = ax / 9.80665 # Convert m/s² to G
|
||||
ay_g = ay / 9.80665
|
||||
az_g = az / 9.80665
|
||||
axp = int((ax_g * 100 + 100)/2) # Map ±1G to 0-100
|
||||
ayp = int((ay_g * 100 + 100)/2)
|
||||
azp = int((az_g * 100 + 100)/2)
|
||||
|
||||
# Gyro already in deg/s, map ±200 DPS to 0-100
|
||||
gx, gy, gz = gyro
|
||||
gx = self.convert_percentage(gx)
|
||||
gy = self.convert_percentage(gy)
|
||||
gz = self.convert_percentage(gz)
|
||||
|
||||
if temp is not None:
|
||||
self.templabel.set_text(f"IMU chip temperature: {temp:.2f}°C")
|
||||
else:
|
||||
self.templabel.set_text("IMU active (no temperature sensor)")
|
||||
else:
|
||||
# Sensor read failed, show random data
|
||||
import random
|
||||
randomnr = random.randint(0,100)
|
||||
axp = randomnr
|
||||
ayp = 50
|
||||
azp = 75
|
||||
gx = 45
|
||||
gy = 50
|
||||
gz = 55
|
||||
else:
|
||||
#temp = 12.34
|
||||
# No sensors available, show random data
|
||||
import random
|
||||
randomnr = random.randint(0,100)
|
||||
axp = randomnr
|
||||
@@ -92,6 +122,7 @@ class IMU(Activity):
|
||||
gx = 45
|
||||
gy = 50
|
||||
gz = 55
|
||||
|
||||
self.sliderx.set_value(axp, False)
|
||||
self.slidery.set_value(ayp, False)
|
||||
self.sliderz.set_value(azp, False)
|
||||
|
||||
@@ -317,7 +317,15 @@ import mpos.lights as LightsManager
|
||||
# Initialize 5 NeoPixel LEDs (GPIO 12)
|
||||
LightsManager.init(neopixel_pin=12, num_leds=5)
|
||||
|
||||
print("Fri3d hardware: Audio and LEDs initialized")
|
||||
# === SENSOR HARDWARE ===
|
||||
import mpos.sensor_manager as SensorManager
|
||||
|
||||
# Create I2C bus for IMU (different pins from display)
|
||||
from machine import I2C
|
||||
imu_i2c = I2C(0, sda=Pin(9), scl=Pin(18))
|
||||
SensorManager.init(imu_i2c, address=0x6B)
|
||||
|
||||
print("Fri3d hardware: Audio, LEDs, and sensors initialized")
|
||||
|
||||
# === STARTUP "WOW" EFFECT ===
|
||||
import time
|
||||
@@ -375,7 +383,7 @@ def startup_wow_effect():
|
||||
except Exception as e:
|
||||
print(f"Startup effect error: {e}")
|
||||
|
||||
_thread.stack_size(mpos.apps.good_stack_size())
|
||||
_thread.stack_size(mpos.apps.good_stack_size()) # default stack size won't work, crashes!
|
||||
_thread.start_new_thread(startup_wow_effect, ())
|
||||
|
||||
print("boot.py finished")
|
||||
|
||||
@@ -110,6 +110,11 @@ AudioFlinger.init(
|
||||
# Note: Desktop builds have no LED hardware
|
||||
# LightsManager will not be initialized (functions will return False)
|
||||
|
||||
# === SENSOR HARDWARE ===
|
||||
# Note: Desktop builds have no sensor hardware
|
||||
import mpos.sensor_manager as SensorManager
|
||||
# Don't call init() - SensorManager functions will return None/False
|
||||
|
||||
print("linux.py finished")
|
||||
|
||||
|
||||
|
||||
@@ -126,4 +126,11 @@ AudioFlinger.init(
|
||||
# Note: Waveshare board has no NeoPixel LEDs
|
||||
# LightsManager will not be initialized (functions will return False)
|
||||
|
||||
# === SENSOR HARDWARE ===
|
||||
import mpos.sensor_manager as SensorManager
|
||||
|
||||
# IMU is on I2C0 (same bus as touch): SDA=48, SCL=47, addr=0x6B
|
||||
# i2c_bus was created on line 75 for touch, reuse it for IMU
|
||||
SensorManager.init(i2c_bus, address=0x6B)
|
||||
|
||||
print("boot.py finished")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# IMU and sensor drivers for MicroPythonOS
|
||||
@@ -0,0 +1,435 @@
|
||||
"""WSEN_ISDS 6-axis IMU driver for MicroPython.
|
||||
|
||||
This driver is for the Würth Elektronik WSEN-ISDS IMU sensor.
|
||||
Source: https://github.com/Fri3dCamp/badge_2024_micropython/pull/10
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Fri3d Camp contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class Wsen_Isds:
|
||||
"""Driver for WSEN-ISDS 6-axis IMU (accelerometer + gyroscope)."""
|
||||
|
||||
_ISDS_STATUS_REG = 0x1E # Status data register
|
||||
_ISDS_WHO_AM_I = 0x0F # WHO_AM_I register
|
||||
|
||||
_REG_G_X_OUT_L = 0x22
|
||||
_REG_G_Y_OUT_L = 0x24
|
||||
_REG_G_Z_OUT_L = 0x26
|
||||
|
||||
_REG_A_X_OUT_L = 0x28
|
||||
_REG_A_Y_OUT_L = 0x2A
|
||||
_REG_A_Z_OUT_L = 0x2C
|
||||
|
||||
_REG_A_TAP_CFG = 0x58
|
||||
|
||||
_options = {
|
||||
'acc_range': {
|
||||
'reg': 0x10, 'mask': 0b11110011, 'shift_left': 2,
|
||||
'val_to_bits': {"2g": 0b00, "4g": 0b10, "8g": 0b11, "16g": 0b01}
|
||||
},
|
||||
'acc_data_rate': {
|
||||
'reg': 0x10, 'mask': 0b00001111, 'shift_left': 4,
|
||||
'val_to_bits': {
|
||||
"0": 0b0000, "1.6Hz": 0b1011, "12.5Hz": 0b0001,
|
||||
"26Hz": 0b0010, "52Hz": 0b0011, "104Hz": 0b0100,
|
||||
"208Hz": 0b0101, "416Hz": 0b0110, "833Hz": 0b0111,
|
||||
"1.66kHz": 0b1000, "3.33kHz": 0b1001, "6.66kHz": 0b1010}
|
||||
},
|
||||
'gyro_range': {
|
||||
'reg': 0x11, 'mask': 0b11110000, 'shift_left': 0,
|
||||
'val_to_bits': {
|
||||
"125dps": 0b0010, "250dps": 0b0000,
|
||||
"500dps": 0b0100, "1000dps": 0b1000, "2000dps": 0b1100}
|
||||
},
|
||||
'gyro_data_rate': {
|
||||
'reg': 0x11, 'mask': 0b00001111, 'shift_left': 4,
|
||||
'val_to_bits': {
|
||||
"0": 0b0000, "12.5Hz": 0b0001, "26Hz": 0b0010,
|
||||
"52Hz": 0b0011, "104Hz": 0b0100, "208Hz": 0b0101,
|
||||
"416Hz": 0b0110, "833Hz": 0b0111, "1.66kHz": 0b1000,
|
||||
"3.33kHz": 0b1001, "6.66kHz": 0b1010}
|
||||
},
|
||||
'tap_double_enable': {
|
||||
'reg': 0x5B, 'mask': 0b01111111, 'shift_left': 7,
|
||||
'val_to_bits': {True: 0b01, False: 0b00}
|
||||
},
|
||||
'tap_threshold': {
|
||||
'reg': 0x59, 'mask': 0b11100000, 'shift_left': 0,
|
||||
'val_to_bits': {0: 0b00, 1: 0b01, 2: 0b10, 3: 0b11, 4: 0b100, 5: 0b101,
|
||||
6: 0b110, 7: 0b111, 8: 0b1000, 9: 0b1001}
|
||||
},
|
||||
'tap_quiet_time': {
|
||||
'reg': 0x5A, 'mask': 0b11110011, 'shift_left': 2,
|
||||
'val_to_bits': {0: 0b00, 1: 0b01, 2: 0b10, 3: 0b11}
|
||||
},
|
||||
'tap_duration_time': {
|
||||
'reg': 0x5A, 'mask': 0b00001111, 'shift_left': 2,
|
||||
'val_to_bits': {0: 0b00, 1: 0b01, 2: 0b10, 3: 0b11, 4: 0b100, 5: 0b101,
|
||||
6: 0b110, 7: 0b111, 8: 0b1000, 9: 0b1001}
|
||||
},
|
||||
'tap_shock_time': {
|
||||
'reg': 0x5A, 'mask': 0b11111100, 'shift_left': 0,
|
||||
'val_to_bits': {0: 0b00, 1: 0b01, 2: 0b10, 3: 0b11}
|
||||
},
|
||||
'tap_single_to_int0': {
|
||||
'reg': 0x5E, 'mask': 0b10111111, 'shift_left': 6,
|
||||
'val_to_bits': {0: 0b00, 1: 0b01}
|
||||
},
|
||||
'tap_double_to_int0': {
|
||||
'reg': 0x5E, 'mask': 0b11110111, 'shift_left': 3,
|
||||
'val_to_bits': {0: 0b00, 1: 0b01}
|
||||
},
|
||||
'int1_on_int0': {
|
||||
'reg': 0x13, 'mask': 0b11011111, 'shift_left': 5,
|
||||
'val_to_bits': {0: 0b00, 1: 0b01}
|
||||
},
|
||||
'ctrl_do_soft_reset': {
|
||||
'reg': 0x12, 'mask': 0b11111110, 'shift_left': 0,
|
||||
'val_to_bits': {True: 0b01, False: 0b00}
|
||||
},
|
||||
'ctrl_do_reboot': {
|
||||
'reg': 0x12, 'mask': 0b01111111, 'shift_left': 7,
|
||||
'val_to_bits': {True: 0b01, False: 0b00}
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, i2c, address=0x6B, acc_range="2g", acc_data_rate="1.6Hz",
|
||||
gyro_range="125dps", gyro_data_rate="12.5Hz"):
|
||||
"""Initialize WSEN-ISDS IMU.
|
||||
|
||||
Args:
|
||||
i2c: I2C bus instance
|
||||
address: I2C address (default 0x6B)
|
||||
acc_range: Accelerometer range ("2g", "4g", "8g", "16g")
|
||||
acc_data_rate: Accelerometer data rate ("0", "1.6Hz", "12.5Hz", ...)
|
||||
gyro_range: Gyroscope range ("125dps", "250dps", "500dps", "1000dps", "2000dps")
|
||||
gyro_data_rate: Gyroscope data rate ("0", "12.5Hz", "26Hz", ...)
|
||||
"""
|
||||
self.i2c = i2c
|
||||
self.address = address
|
||||
|
||||
self.acc_offset_x = 0
|
||||
self.acc_offset_y = 0
|
||||
self.acc_offset_z = 0
|
||||
self.acc_range = 0
|
||||
self.acc_sensitivity = 0
|
||||
|
||||
self.gyro_offset_x = 0
|
||||
self.gyro_offset_y = 0
|
||||
self.gyro_offset_z = 0
|
||||
self.gyro_range = 0
|
||||
self.gyro_sensitivity = 0
|
||||
|
||||
self.ACC_NUM_SAMPLES_CALIBRATION = 5
|
||||
self.ACC_CALIBRATION_DELAY_MS = 10
|
||||
|
||||
self.GYRO_NUM_SAMPLES_CALIBRATION = 5
|
||||
self.GYRO_CALIBRATION_DELAY_MS = 10
|
||||
|
||||
self.set_acc_range(acc_range)
|
||||
self.set_acc_data_rate(acc_data_rate)
|
||||
self.set_gyro_range(gyro_range)
|
||||
self.set_gyro_data_rate(gyro_data_rate)
|
||||
|
||||
def get_chip_id(self):
|
||||
"""Get chip ID for detection. Returns WHO_AM_I register value."""
|
||||
try:
|
||||
return self.i2c.readfrom_mem(self.address, self._ISDS_WHO_AM_I, 1)[0]
|
||||
except:
|
||||
return 0
|
||||
|
||||
def _write_option(self, option, value):
|
||||
"""Write configuration option to sensor register."""
|
||||
opt = Wsen_Isds._options[option]
|
||||
try:
|
||||
bits = opt["val_to_bits"][value]
|
||||
config_value = self.i2c.readfrom_mem(self.address, opt["reg"], 1)[0]
|
||||
config_value &= opt["mask"]
|
||||
config_value |= (bits << opt["shift_left"])
|
||||
self.i2c.writeto_mem(self.address, opt["reg"], bytes([config_value]))
|
||||
except KeyError as err:
|
||||
print(f"Invalid option: {option}, or invalid option value: {value}.", err)
|
||||
|
||||
def set_acc_range(self, acc_range):
|
||||
"""Set accelerometer range."""
|
||||
self._write_option('acc_range', acc_range)
|
||||
self.acc_range = acc_range
|
||||
self._acc_calc_sensitivity()
|
||||
|
||||
def set_acc_data_rate(self, acc_rate):
|
||||
"""Set accelerometer data rate."""
|
||||
self._write_option('acc_data_rate', acc_rate)
|
||||
|
||||
def set_gyro_range(self, gyro_range):
|
||||
"""Set gyroscope range."""
|
||||
self._write_option('gyro_range', gyro_range)
|
||||
self.gyro_range = gyro_range
|
||||
self._gyro_calc_sensitivity()
|
||||
|
||||
def set_gyro_data_rate(self, gyro_rate):
|
||||
"""Set gyroscope data rate."""
|
||||
self._write_option('gyro_data_rate', gyro_rate)
|
||||
|
||||
def _gyro_calc_sensitivity(self):
|
||||
"""Calculate gyroscope sensitivity based on range."""
|
||||
sensitivity_mapping = {
|
||||
"125dps": 4.375,
|
||||
"250dps": 8.75,
|
||||
"500dps": 17.5,
|
||||
"1000dps": 35,
|
||||
"2000dps": 70
|
||||
}
|
||||
|
||||
if self.gyro_range in sensitivity_mapping:
|
||||
self.gyro_sensitivity = sensitivity_mapping[self.gyro_range]
|
||||
else:
|
||||
print("Invalid range value:", self.gyro_range)
|
||||
|
||||
def soft_reset(self):
|
||||
"""Perform soft reset of the sensor."""
|
||||
self._write_option('ctrl_do_soft_reset', True)
|
||||
|
||||
def reboot(self):
|
||||
"""Reboot the sensor."""
|
||||
self._write_option('ctrl_do_reboot', True)
|
||||
|
||||
def set_interrupt(self, interrupts_enable=False, inact_en=False, slope_fds=False,
|
||||
tap_x_en=True, tap_y_en=True, tap_z_en=True):
|
||||
"""Configure interrupt for tap gestures on INT0 pad."""
|
||||
config_value = 0b00000000
|
||||
|
||||
if interrupts_enable:
|
||||
config_value |= (1 << 7)
|
||||
if inact_en:
|
||||
inact_en = 0x01
|
||||
config_value |= (inact_en << 5)
|
||||
if slope_fds:
|
||||
config_value |= (1 << 4)
|
||||
if tap_x_en:
|
||||
config_value |= (1 << 3)
|
||||
if tap_y_en:
|
||||
config_value |= (1 << 2)
|
||||
if tap_z_en:
|
||||
config_value |= (1 << 1)
|
||||
|
||||
self.i2c.writeto_mem(self.address, Wsen_Isds._REG_A_TAP_CFG,
|
||||
bytes([config_value]))
|
||||
|
||||
self._write_option('tap_double_enable', False)
|
||||
self._write_option('tap_threshold', 9)
|
||||
self._write_option('tap_quiet_time', 1)
|
||||
self._write_option('tap_duration_time', 5)
|
||||
self._write_option('tap_shock_time', 2)
|
||||
self._write_option('tap_single_to_int0', 1)
|
||||
self._write_option('tap_double_to_int0', 1)
|
||||
self._write_option('int1_on_int0', 1)
|
||||
|
||||
def acc_calibrate(self, samples=None):
|
||||
"""Calibrate accelerometer by averaging samples while device is stationary.
|
||||
|
||||
Args:
|
||||
samples: Number of samples to average (default: ACC_NUM_SAMPLES_CALIBRATION)
|
||||
"""
|
||||
if samples is None:
|
||||
samples = self.ACC_NUM_SAMPLES_CALIBRATION
|
||||
|
||||
self.acc_offset_x = 0
|
||||
self.acc_offset_y = 0
|
||||
self.acc_offset_z = 0
|
||||
|
||||
for _ in range(samples):
|
||||
x, y, z = self._read_raw_accelerations()
|
||||
self.acc_offset_x += x
|
||||
self.acc_offset_y += y
|
||||
self.acc_offset_z += z
|
||||
time.sleep_ms(self.ACC_CALIBRATION_DELAY_MS)
|
||||
|
||||
self.acc_offset_x //= samples
|
||||
self.acc_offset_y //= samples
|
||||
self.acc_offset_z //= samples
|
||||
|
||||
def _acc_calc_sensitivity(self):
|
||||
"""Calculate accelerometer sensitivity based on range (in mg/digit)."""
|
||||
sensitivity_mapping = {
|
||||
"2g": 0.061,
|
||||
"4g": 0.122,
|
||||
"8g": 0.244,
|
||||
"16g": 0.488
|
||||
}
|
||||
if self.acc_range in sensitivity_mapping:
|
||||
self.acc_sensitivity = sensitivity_mapping[self.acc_range]
|
||||
else:
|
||||
print("Invalid range value:", self.acc_range)
|
||||
|
||||
def read_accelerations(self):
|
||||
"""Read calibrated accelerometer data.
|
||||
|
||||
Returns:
|
||||
Tuple (x, y, z) in mg (milligrams)
|
||||
"""
|
||||
raw_a_x, raw_a_y, raw_a_z = self._read_raw_accelerations()
|
||||
|
||||
a_x = (raw_a_x - self.acc_offset_x) * self.acc_sensitivity
|
||||
a_y = (raw_a_y - self.acc_offset_y) * self.acc_sensitivity
|
||||
a_z = (raw_a_z - self.acc_offset_z) * self.acc_sensitivity
|
||||
|
||||
return a_x, a_y, a_z
|
||||
|
||||
def _read_raw_accelerations(self):
|
||||
"""Read raw accelerometer data."""
|
||||
if not self._acc_data_ready():
|
||||
raise Exception("sensor data not ready")
|
||||
|
||||
raw = self.i2c.readfrom_mem(self.address, Wsen_Isds._REG_A_X_OUT_L, 6)
|
||||
|
||||
raw_a_x = self._convert_from_raw(raw[0], raw[1])
|
||||
raw_a_y = self._convert_from_raw(raw[2], raw[3])
|
||||
raw_a_z = self._convert_from_raw(raw[4], raw[5])
|
||||
|
||||
return raw_a_x, raw_a_y, raw_a_z
|
||||
|
||||
def gyro_calibrate(self, samples=None):
|
||||
"""Calibrate gyroscope by averaging samples while device is stationary.
|
||||
|
||||
Args:
|
||||
samples: Number of samples to average (default: GYRO_NUM_SAMPLES_CALIBRATION)
|
||||
"""
|
||||
if samples is None:
|
||||
samples = self.GYRO_NUM_SAMPLES_CALIBRATION
|
||||
|
||||
self.gyro_offset_x = 0
|
||||
self.gyro_offset_y = 0
|
||||
self.gyro_offset_z = 0
|
||||
|
||||
for _ in range(samples):
|
||||
x, y, z = self._read_raw_angular_velocities()
|
||||
self.gyro_offset_x += x
|
||||
self.gyro_offset_y += y
|
||||
self.gyro_offset_z += z
|
||||
time.sleep_ms(self.GYRO_CALIBRATION_DELAY_MS)
|
||||
|
||||
self.gyro_offset_x //= samples
|
||||
self.gyro_offset_y //= samples
|
||||
self.gyro_offset_z //= samples
|
||||
|
||||
def read_angular_velocities(self):
|
||||
"""Read calibrated gyroscope data.
|
||||
|
||||
Returns:
|
||||
Tuple (x, y, z) in mdps (milli-degrees per second)
|
||||
"""
|
||||
raw_g_x, raw_g_y, raw_g_z = self._read_raw_angular_velocities()
|
||||
|
||||
g_x = (raw_g_x - self.gyro_offset_x) * self.gyro_sensitivity
|
||||
g_y = (raw_g_y - self.gyro_offset_y) * self.gyro_sensitivity
|
||||
g_z = (raw_g_z - self.gyro_offset_z) * self.gyro_sensitivity
|
||||
|
||||
return g_x, g_y, g_z
|
||||
|
||||
def _read_raw_angular_velocities(self):
|
||||
"""Read raw gyroscope data."""
|
||||
if not self._gyro_data_ready():
|
||||
raise Exception("sensor data not ready")
|
||||
|
||||
raw = self.i2c.readfrom_mem(self.address, Wsen_Isds._REG_G_X_OUT_L, 6)
|
||||
|
||||
raw_g_x = self._convert_from_raw(raw[0], raw[1])
|
||||
raw_g_y = self._convert_from_raw(raw[2], raw[3])
|
||||
raw_g_z = self._convert_from_raw(raw[4], raw[5])
|
||||
|
||||
return raw_g_x, raw_g_y, raw_g_z
|
||||
|
||||
def read_angular_velocities_accelerations(self):
|
||||
"""Read both gyroscope and accelerometer in one call.
|
||||
|
||||
Returns:
|
||||
Tuple (gx, gy, gz, ax, ay, az) where gyro is in mdps, accel is in mg
|
||||
"""
|
||||
raw_g_x, raw_g_y, raw_g_z, raw_a_x, raw_a_y, raw_a_z = \
|
||||
self._read_raw_gyro_acc()
|
||||
|
||||
g_x = (raw_g_x - self.gyro_offset_x) * self.gyro_sensitivity
|
||||
g_y = (raw_g_y - self.gyro_offset_y) * self.gyro_sensitivity
|
||||
g_z = (raw_g_z - self.gyro_offset_z) * self.gyro_sensitivity
|
||||
|
||||
a_x = (raw_a_x - self.acc_offset_x) * self.acc_sensitivity
|
||||
a_y = (raw_a_y - self.acc_offset_y) * self.acc_sensitivity
|
||||
a_z = (raw_a_z - self.acc_offset_z) * self.acc_sensitivity
|
||||
|
||||
return g_x, g_y, g_z, a_x, a_y, a_z
|
||||
|
||||
def _read_raw_gyro_acc(self):
|
||||
"""Read raw gyroscope and accelerometer data in one call."""
|
||||
acc_data_ready, gyro_data_ready = self._acc_gyro_data_ready()
|
||||
if not acc_data_ready or not gyro_data_ready:
|
||||
raise Exception("sensor data not ready")
|
||||
|
||||
raw = self.i2c.readfrom_mem(self.address, Wsen_Isds._REG_G_X_OUT_L, 12)
|
||||
|
||||
raw_g_x = self._convert_from_raw(raw[0], raw[1])
|
||||
raw_g_y = self._convert_from_raw(raw[2], raw[3])
|
||||
raw_g_z = self._convert_from_raw(raw[4], raw[5])
|
||||
|
||||
raw_a_x = self._convert_from_raw(raw[6], raw[7])
|
||||
raw_a_y = self._convert_from_raw(raw[8], raw[9])
|
||||
raw_a_z = self._convert_from_raw(raw[10], raw[11])
|
||||
|
||||
return raw_g_x, raw_g_y, raw_g_z, raw_a_x, raw_a_y, raw_a_z
|
||||
|
||||
@staticmethod
|
||||
def _convert_from_raw(b_l, b_h):
|
||||
"""Convert two bytes (little-endian) to signed 16-bit integer."""
|
||||
c = (b_h << 8) | b_l
|
||||
if c & (1 << 15):
|
||||
c -= 1 << 16
|
||||
return c
|
||||
|
||||
def _acc_data_ready(self):
|
||||
"""Check if accelerometer data is ready."""
|
||||
return self._get_status_reg()[0]
|
||||
|
||||
def _gyro_data_ready(self):
|
||||
"""Check if gyroscope data is ready."""
|
||||
return self._get_status_reg()[1]
|
||||
|
||||
def _acc_gyro_data_ready(self):
|
||||
"""Check if both accelerometer and gyroscope data are ready."""
|
||||
status_reg = self._get_status_reg()
|
||||
return status_reg[0], status_reg[1]
|
||||
|
||||
def _get_status_reg(self):
|
||||
"""Read status register.
|
||||
|
||||
Returns:
|
||||
Tuple (acc_data_ready, gyro_data_ready, temp_data_ready)
|
||||
"""
|
||||
raw = self.i2c.readfrom_mem(self.address, Wsen_Isds._ISDS_STATUS_REG, 4)
|
||||
|
||||
acc_data_ready = True if raw[0] == 1 else False
|
||||
gyro_data_ready = True if raw[1] == 1 else False
|
||||
temp_data_ready = True if raw[2] == 1 else False
|
||||
|
||||
return acc_data_ready, gyro_data_ready, temp_data_ready
|
||||
File diff suppressed because it is too large
Load Diff
@@ -163,16 +163,22 @@ def create_notification_bar():
|
||||
else:
|
||||
wifi_icon.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
|
||||
can_check_temperature = False
|
||||
try:
|
||||
import esp32
|
||||
can_check_temperature = True
|
||||
except Exception as e:
|
||||
print("Warning: can't check temperature sensor:", str(e))
|
||||
|
||||
# Get temperature sensor via SensorManager
|
||||
import mpos.sensor_manager as SensorManager
|
||||
temp_sensor = None
|
||||
if SensorManager.is_available():
|
||||
# Prefer MCU temperature (more stable) over IMU temperature
|
||||
temp_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_SOC_TEMPERATURE)
|
||||
if not temp_sensor:
|
||||
temp_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_IMU_TEMPERATURE)
|
||||
|
||||
def update_temperature(timer):
|
||||
if can_check_temperature:
|
||||
temp_label.set_text(f"{esp32.mcu_temperature()}°C")
|
||||
if temp_sensor:
|
||||
temp = SensorManager.read_sensor(temp_sensor)
|
||||
if temp is not None:
|
||||
temp_label.set_text(f"{round(temp)}°C")
|
||||
else:
|
||||
temp_label.set_text("--°C")
|
||||
else:
|
||||
temp_label.set_text("42°C")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user