Skip to content

Commit ce981d7

Browse files
Fix unit tests
1 parent 4e7baf4 commit ce981d7

File tree

10 files changed

+1299
-33
lines changed

10 files changed

+1299
-33
lines changed

CLAUDE.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,8 @@ Current stable version: 0.3.3 (as of latest CHANGELOG entry)
449449
- Config/preferences: `internal_filesystem/lib/mpos/config.py`
450450
- Top menu/drawer: `internal_filesystem/lib/mpos/ui/topmenu.py`
451451
- Activity navigation: `internal_filesystem/lib/mpos/activity_navigator.py`
452+
- Sensor management: `internal_filesystem/lib/mpos/sensor_manager.py`
453+
- IMU drivers: `internal_filesystem/lib/mpos/hardware/drivers/qmi8658.py` and `wsen_isds.py`
452454

453455
## Common Utilities and Helpers
454456

@@ -642,6 +644,7 @@ def defocus_handler(self, obj):
642644
- `mpos.sdcard.SDCardManager`: SD card mounting and management
643645
- `mpos.clipboard`: System clipboard access
644646
- `mpos.battery_voltage`: Battery level reading (ESP32 only)
647+
- `mpos.sensor_manager`: Unified sensor access (accelerometer, gyroscope, temperature)
645648

646649
## Audio System (AudioFlinger)
647650

@@ -849,6 +852,173 @@ class LEDAnimationActivity(Activity):
849852
- **Thread-safe**: No locking (single-threaded usage recommended)
850853
- **Desktop**: Functions return `False` (no-op) on desktop builds
851854

855+
## Sensor System (SensorManager)
856+
857+
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.
858+
859+
### Supported Sensors
860+
861+
**IMU Sensors:**
862+
- **QMI8658** (Waveshare ESP32-S3): Accelerometer, Gyroscope, Temperature
863+
- **WSEN_ISDS** (Fri3d Camp 2024 Badge): Accelerometer, Gyroscope
864+
865+
**Temperature Sensors:**
866+
- **ESP32 MCU Temperature**: Internal SoC temperature sensor
867+
- **IMU Chip Temperature**: QMI8658 chip temperature
868+
869+
### Basic Usage
870+
871+
**Check availability and read sensors**:
872+
```python
873+
import mpos.sensor_manager as SensorManager
874+
875+
# Check if sensors are available
876+
if SensorManager.is_available():
877+
# Get sensors
878+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
879+
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
880+
temp = SensorManager.get_default_sensor(SensorManager.TYPE_SOC_TEMPERATURE)
881+
882+
# Read data (returns standard SI units)
883+
accel_data = SensorManager.read_sensor(accel) # Returns (x, y, z) in m/s²
884+
gyro_data = SensorManager.read_sensor(gyro) # Returns (x, y, z) in deg/s
885+
temperature = SensorManager.read_sensor(temp) # Returns °C
886+
887+
if accel_data:
888+
ax, ay, az = accel_data
889+
print(f"Acceleration: {ax:.2f}, {ay:.2f}, {az:.2f} m/s²")
890+
```
891+
892+
### Sensor Types
893+
894+
```python
895+
# Motion sensors
896+
SensorManager.TYPE_ACCELEROMETER # m/s² (meters per second squared)
897+
SensorManager.TYPE_GYROSCOPE # deg/s (degrees per second)
898+
899+
# Temperature sensors
900+
SensorManager.TYPE_SOC_TEMPERATURE # °C (MCU internal temperature)
901+
SensorManager.TYPE_IMU_TEMPERATURE # °C (IMU chip temperature)
902+
```
903+
904+
### Tilt-Controlled Game Example
905+
906+
```python
907+
from mpos.app.activity import Activity
908+
import mpos.sensor_manager as SensorManager
909+
import mpos.ui
910+
import time
911+
912+
class TiltBallActivity(Activity):
913+
def onCreate(self):
914+
self.screen = lv.obj()
915+
916+
# Get accelerometer
917+
self.accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
918+
919+
# Create ball UI
920+
self.ball = lv.obj(self.screen)
921+
self.ball.set_size(20, 20)
922+
self.ball.set_style_radius(10, 0)
923+
924+
# Physics state
925+
self.ball_x = 160.0
926+
self.ball_y = 120.0
927+
self.ball_vx = 0.0
928+
self.ball_vy = 0.0
929+
self.last_time = time.ticks_ms()
930+
931+
self.setContentView(self.screen)
932+
933+
def onResume(self, screen):
934+
self.last_time = time.ticks_ms()
935+
mpos.ui.task_handler.add_event_cb(self.update_physics, 1)
936+
937+
def onPause(self, screen):
938+
mpos.ui.task_handler.remove_event_cb(self.update_physics)
939+
940+
def update_physics(self, a, b):
941+
current_time = time.ticks_ms()
942+
delta_time = time.ticks_diff(current_time, self.last_time) / 1000.0
943+
self.last_time = current_time
944+
945+
# Read accelerometer
946+
accel = SensorManager.read_sensor(self.accel)
947+
if accel:
948+
ax, ay, az = accel
949+
950+
# Apply acceleration to velocity
951+
self.ball_vx += (ax * 5.0) * delta_time
952+
self.ball_vy -= (ay * 5.0) * delta_time # Flip Y
953+
954+
# Update position
955+
self.ball_x += self.ball_vx
956+
self.ball_y += self.ball_vy
957+
958+
# Update ball position
959+
self.ball.set_pos(int(self.ball_x), int(self.ball_y))
960+
```
961+
962+
### Calibration
963+
964+
Calibration removes sensor drift and improves accuracy. The device must be **stationary** during calibration.
965+
966+
```python
967+
# Calibrate accelerometer and gyroscope
968+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
969+
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
970+
971+
# Calibrate (100 samples, device must be flat and still)
972+
accel_offsets = SensorManager.calibrate_sensor(accel, samples=100)
973+
gyro_offsets = SensorManager.calibrate_sensor(gyro, samples=100)
974+
975+
# Calibration is automatically saved to SharedPreferences
976+
# and loaded on next boot
977+
```
978+
979+
### Performance Recommendations
980+
981+
**Polling rate recommendations:**
982+
- **Games**: 20-30 Hz (responsive but not excessive)
983+
- **UI feedback**: 10-15 Hz (smooth for tilt UI)
984+
- **Background monitoring**: 1-5 Hz (screen rotation, pedometer)
985+
986+
```python
987+
# ❌ BAD: Poll every frame (60 Hz)
988+
def update_frame(self, a, b):
989+
accel = SensorManager.read_sensor(self.accel) # Too frequent!
990+
991+
# ✅ GOOD: Poll every other frame (30 Hz)
992+
def update_frame(self, a, b):
993+
self.frame_count += 1
994+
if self.frame_count % 2 == 0:
995+
accel = SensorManager.read_sensor(self.accel)
996+
```
997+
998+
### Hardware Support Matrix
999+
1000+
| Platform | Accelerometer | Gyroscope | IMU Temp | MCU Temp |
1001+
|----------|---------------|-----------|----------|----------|
1002+
| Waveshare ESP32-S3 | ✅ QMI8658 | ✅ QMI8658 | ✅ QMI8658 | ✅ ESP32 |
1003+
| Fri3d 2024 Badge | ✅ WSEN_ISDS | ✅ WSEN_ISDS || ✅ ESP32 |
1004+
| Desktop/Linux |||||
1005+
1006+
### Implementation Details
1007+
1008+
- **Location**: `lib/mpos/sensor_manager.py`
1009+
- **Pattern**: Module-level singleton (similar to `battery_voltage.py`)
1010+
- **Units**: Standard SI (m/s² for acceleration, deg/s for gyroscope, °C for temperature)
1011+
- **Calibration**: Persistent via SharedPreferences (`data/com.micropythonos.sensors/config.json`)
1012+
- **Thread-safe**: Uses locks for concurrent access
1013+
- **Auto-detection**: Identifies IMU type via chip ID registers
1014+
- **Desktop**: Functions return `None` (graceful fallback) on desktop builds
1015+
1016+
### Driver Locations
1017+
1018+
- **QMI8658**: `lib/mpos/hardware/drivers/qmi8658.py`
1019+
- **WSEN_ISDS**: `lib/mpos/hardware/drivers/wsen_isds.py`
1020+
- **Board init**: `lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py` and `lib/mpos/board/fri3d_2024.py`
1021+
8521022
## Animations and Game Loops
8531023

8541024
MicroPythonOS supports frame-based animations and game loops using the TaskHandler event system. This pattern is used for games, particle effects, and smooth animations.

internal_filesystem/apps/com.micropythonos.imu/assets/imu.py

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from mpos.apps import Activity
2+
import mpos.sensor_manager as SensorManager
23

34
class IMU(Activity):
45

5-
sensor = None
6+
accel_sensor = None
7+
gyro_sensor = None
8+
temp_sensor = None
69
refresh_timer = None
710

811
# widgets:
@@ -30,12 +33,16 @@ def onCreate(self):
3033
self.slidergz = lv.slider(screen)
3134
self.slidergz.align(lv.ALIGN.CENTER, 0, 90)
3235
try:
33-
from machine import Pin, I2C
34-
from qmi8658 import QMI8658
35-
import machine
36-
self.sensor = QMI8658(I2C(0, sda=machine.Pin(48), scl=machine.Pin(47)))
37-
print("IMU sensor initialized")
38-
#print(f"{self.sensor.temperature=} {self.sensor.acceleration=} {self.sensor.gyro=}")
36+
if SensorManager.is_available():
37+
self.accel_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
38+
self.gyro_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
39+
# Get IMU temperature (not MCU temperature)
40+
self.temp_sensor = SensorManager.get_default_sensor(SensorManager.TYPE_IMU_TEMPERATURE)
41+
print("IMU sensors initialized via SensorManager")
42+
print(f"Available sensors: {SensorManager.get_sensor_list()}")
43+
else:
44+
print("Warning: No IMU sensors available")
45+
self.templabel.set_text("No IMU sensors available")
3946
except Exception as e:
4047
warning = f"Warning: could not initialize IMU hardware:\n{e}"
4148
print(warning)
@@ -68,22 +75,45 @@ def convert_percentage(self, value: float) -> int:
6875

6976
def refresh(self, timer):
7077
#print("refresh timer")
71-
if self.sensor:
72-
#print(f"{self.sensor.temperature=} {self.sensor.acceleration=} {self.sensor.gyro=}")
73-
temp = self.sensor.temperature
74-
ax = self.sensor.acceleration[0]
75-
axp = int((ax * 100 + 100)/2)
76-
ay = self.sensor.acceleration[1]
77-
ayp = int((ay * 100 + 100)/2)
78-
az = self.sensor.acceleration[2]
79-
azp = int((az * 100 + 100)/2)
80-
# values between -200 and 200 => /4 becomes -50 and 50 => +50 becomes 0 and 100
81-
gx = self.convert_percentage(self.sensor.gyro[0])
82-
gy = self.convert_percentage(self.sensor.gyro[1])
83-
gz = self.convert_percentage(self.sensor.gyro[2])
84-
self.templabel.set_text(f"IMU chip temperature: {temp:.2f}°C")
78+
if self.accel_sensor and self.gyro_sensor:
79+
# Read sensor data via SensorManager (returns m/s² for accel, deg/s for gyro)
80+
accel = SensorManager.read_sensor(self.accel_sensor)
81+
gyro = SensorManager.read_sensor(self.gyro_sensor)
82+
temp = SensorManager.read_sensor(self.temp_sensor) if self.temp_sensor else None
83+
84+
if accel and gyro:
85+
# Convert m/s² to G for display (divide by 9.80665)
86+
# Range: ±8G → ±1G = ±10% of range → map to 0-100
87+
ax, ay, az = accel
88+
ax_g = ax / 9.80665 # Convert m/s² to G
89+
ay_g = ay / 9.80665
90+
az_g = az / 9.80665
91+
axp = int((ax_g * 100 + 100)/2) # Map ±1G to 0-100
92+
ayp = int((ay_g * 100 + 100)/2)
93+
azp = int((az_g * 100 + 100)/2)
94+
95+
# Gyro already in deg/s, map ±200 DPS to 0-100
96+
gx, gy, gz = gyro
97+
gx = self.convert_percentage(gx)
98+
gy = self.convert_percentage(gy)
99+
gz = self.convert_percentage(gz)
100+
101+
if temp is not None:
102+
self.templabel.set_text(f"IMU chip temperature: {temp:.2f}°C")
103+
else:
104+
self.templabel.set_text("IMU active (no temperature sensor)")
105+
else:
106+
# Sensor read failed, show random data
107+
import random
108+
randomnr = random.randint(0,100)
109+
axp = randomnr
110+
ayp = 50
111+
azp = 75
112+
gx = 45
113+
gy = 50
114+
gz = 55
85115
else:
86-
#temp = 12.34
116+
# No sensors available, show random data
87117
import random
88118
randomnr = random.randint(0,100)
89119
axp = randomnr
@@ -92,6 +122,7 @@ def refresh(self, timer):
92122
gx = 45
93123
gy = 50
94124
gz = 55
125+
95126
self.sliderx.set_value(axp, False)
96127
self.slidery.set_value(ayp, False)
97128
self.sliderz.set_value(azp, False)

internal_filesystem/lib/mpos/board/fri3d_2024.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,15 @@ def adc_to_voltage(adc_value):
317317
# Initialize 5 NeoPixel LEDs (GPIO 12)
318318
LightsManager.init(neopixel_pin=12, num_leds=5)
319319

320-
print("Fri3d hardware: Audio and LEDs initialized")
320+
# === SENSOR HARDWARE ===
321+
import mpos.sensor_manager as SensorManager
322+
323+
# Create I2C bus for IMU (different pins from display)
324+
from machine import I2C
325+
imu_i2c = I2C(0, sda=Pin(9), scl=Pin(18))
326+
SensorManager.init(imu_i2c, address=0x6B)
327+
328+
print("Fri3d hardware: Audio, LEDs, and sensors initialized")
321329

322330
# === STARTUP "WOW" EFFECT ===
323331
import time
@@ -375,7 +383,7 @@ def startup_wow_effect():
375383
except Exception as e:
376384
print(f"Startup effect error: {e}")
377385

378-
_thread.stack_size(mpos.apps.good_stack_size())
386+
_thread.stack_size(mpos.apps.good_stack_size()) # default stack size won't work, crashes!
379387
_thread.start_new_thread(startup_wow_effect, ())
380388

381389
print("boot.py finished")

internal_filesystem/lib/mpos/board/linux.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ def adc_to_voltage(adc_value):
110110
# Note: Desktop builds have no LED hardware
111111
# LightsManager will not be initialized (functions will return False)
112112

113+
# === SENSOR HARDWARE ===
114+
# Note: Desktop builds have no sensor hardware
115+
import mpos.sensor_manager as SensorManager
116+
# Don't call init() - SensorManager functions will return None/False
117+
113118
print("linux.py finished")
114119

115120

internal_filesystem/lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,11 @@ def adc_to_voltage(adc_value):
126126
# Note: Waveshare board has no NeoPixel LEDs
127127
# LightsManager will not be initialized (functions will return False)
128128

129+
# === SENSOR HARDWARE ===
130+
import mpos.sensor_manager as SensorManager
131+
132+
# IMU is on I2C0 (same bus as touch): SDA=48, SCL=47, addr=0x6B
133+
# i2c_bus was created on line 75 for touch, reuse it for IMU
134+
SensorManager.init(i2c_bus, address=0x6B)
135+
129136
print("boot.py finished")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# IMU and sensor drivers for MicroPythonOS

internal_filesystem/lib/qmi8658.py renamed to internal_filesystem/lib/mpos/hardware/drivers/qmi8658.py

File renamed without changes.

0 commit comments

Comments
 (0)