Fix unit tests

This commit is contained in:
Thomas Farstrike
2025-12-04 13:21:38 +01:00
parent 4e7baf4ec6
commit ce981d790f
10 changed files with 1299 additions and 33 deletions
+170
View File
@@ -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
+15 -9
View File
@@ -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")