Files
MicroPythonOS/tests/test_sensor_manager.py
T
2026-03-17 15:17:38 +01:00

327 lines
12 KiB
Python

# Unit tests for SensorManager service
import unittest
import sys
# Allow importing shared test mocks
sys.path.insert(0, "../tests")
from mocks import (
MockI2C,
MockQMI8658,
MockSharedPreferences,
MockWsenIsds,
make_config_module,
make_machine_i2c_module,
)
# Mock constants from drivers
_QMI8685_PARTID = 0x05
_REG_PARTID = 0x00
_ACCELSCALE_RANGE_8G = 0b10
_GYROSCALE_RANGE_256DPS = 0b100
mock_config = make_config_module(MockSharedPreferences)
# Create mock modules
mock_machine = make_machine_i2c_module(MockI2C)
mock_qmi8658 = type("module", (), {
"QMI8658": MockQMI8658,
"_QMI8685_PARTID": _QMI8685_PARTID,
"_REG_PARTID": _REG_PARTID,
"_ACCELSCALE_RANGE_8G": _ACCELSCALE_RANGE_8G,
"_GYROSCALE_RANGE_256DPS": _GYROSCALE_RANGE_256DPS,
})()
mock_wsen_isds = type("module", (), {"Wsen_Isds": MockWsenIsds})()
# Mock esp32 module
def _mock_mcu_temperature(*args, **kwargs):
"""Mock MCU temperature sensor."""
return 42.0
mock_esp32 = type('module', (), {
'mcu_temperature': _mock_mcu_temperature
})()
# Inject mocks into sys.modules
sys.modules['machine'] = mock_machine
# Mock parent packages for driver imports
# These need to exist for the import path to work
sys.modules['drivers'] = type('module', (), {})()
sys.modules['drivers.imu_sensor'] = type('module', (), {})()
sys.modules['drivers.imu_sensor.qmi8658'] = mock_qmi8658
sys.modules['drivers.imu_sensor.wsen_isds'] = mock_wsen_isds
sys.modules['esp32'] = mock_esp32
sys.modules['mpos.config'] = mock_config
# Mock _thread for thread safety testing
try:
import _thread
except ImportError:
mock_thread = type('module', (), {
'allocate_lock': lambda: type('lock', (), {
'acquire': lambda self: None,
'release': lambda self: None
})()
})()
sys.modules['_thread'] = mock_thread
# Now import the module to test
from mpos import SensorManager
class TestSensorManagerQMI8658(unittest.TestCase):
"""Test cases for SensorManager with QMI8658 IMU."""
def setUp(self):
"""Set up test fixtures before each test."""
# Reset SensorManager singleton instance
SensorManager._instance = None
# Reset SensorManager class state
SensorManager._initialized = False
SensorManager._imu_driver = None
SensorManager._sensor_list = []
SensorManager._has_mcu_temperature = False
SensorManager._i2c_bus = None
SensorManager._i2c_address = None
# Create mock I2C bus with QMI8658
self.i2c_bus = MockI2C(0, sda=48, scl=47)
# Set QMI8658 chip ID
self.i2c_bus.memory[0x6B] = {_REG_PARTID: [_QMI8685_PARTID]}
def test_initialization_qmi8658(self):
"""Test that SensorManager initializes with QMI8658."""
result = SensorManager.init(self.i2c_bus, address=0x6B)
self.assertTrue(result)
self.assertTrue(SensorManager.is_available())
def test_sensor_list_qmi8658(self):
"""Test getting sensor list for QMI8658."""
SensorManager.init(self.i2c_bus, address=0x6B)
sensors = SensorManager.get_sensor_list()
# QMI8658 provides: Accelerometer, Gyroscope, IMU Temperature, MCU Temperature
self.assertGreaterEqual(len(sensors), 3)
# Check sensor types present
sensor_types = [s.type for s in sensors]
self.assertIn(SensorManager.TYPE_ACCELEROMETER, sensor_types)
self.assertIn(SensorManager.TYPE_GYROSCOPE, sensor_types)
self.assertIn(SensorManager.TYPE_IMU_TEMPERATURE, sensor_types)
def test_get_default_sensor(self):
"""Test getting default sensor by type."""
SensorManager.init(self.i2c_bus, address=0x6B)
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
self.assertIsNotNone(accel)
self.assertEqual(accel.type, SensorManager.TYPE_ACCELEROMETER)
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
self.assertIsNotNone(gyro)
self.assertEqual(gyro.type, SensorManager.TYPE_GYROSCOPE)
def test_get_nonexistent_sensor(self):
"""Test getting a sensor type that doesn't exist."""
SensorManager.init(self.i2c_bus, address=0x6B)
# Type 999 doesn't exist
sensor = SensorManager.get_default_sensor(999)
self.assertIsNone(sensor)
def test_read_accelerometer(self):
"""Test reading accelerometer data."""
SensorManager.init(self.i2c_bus, address=0x6B)
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
data = SensorManager.read_sensor(accel)
self.assertTrue(data is not None, f"read_sensor returned None, expected tuple")
self.assertEqual(len(data), 3) # (x, y, z)
ax, ay, az = data
# At rest, Z should be ~9.8 m/s² (1G converted to m/s²)
self.assertAlmostEqual(az, 9.80665, places=2)
def test_read_gyroscope(self):
"""Test reading gyroscope data."""
SensorManager.init(self.i2c_bus, address=0x6B)
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
data = SensorManager.read_sensor(gyro)
self.assertTrue(data is not None, f"read_sensor returned None, expected tuple")
self.assertEqual(len(data), 3) # (x, y, z)
gx, gy, gz = data
# Stationary, all should be ~0 deg/s
self.assertAlmostEqual(gx, 0.0, places=1)
self.assertAlmostEqual(gy, 0.0, places=1)
self.assertAlmostEqual(gz, 0.0, places=1)
def test_read_temperature(self):
"""Test reading temperature data."""
SensorManager.init(self.i2c_bus, address=0x6B)
# Try IMU temperature
imu_temp = SensorManager.get_default_sensor(SensorManager.TYPE_IMU_TEMPERATURE)
if imu_temp:
temp = SensorManager.read_sensor(imu_temp)
self.assertIsNotNone(temp)
self.assertIsInstance(temp, (int, float))
# Try MCU temperature
mcu_temp = SensorManager.get_default_sensor(SensorManager.TYPE_SOC_TEMPERATURE)
if mcu_temp:
temp = SensorManager.read_sensor(mcu_temp)
self.assertIsNotNone(temp)
self.assertEqual(temp, 42.0) # Mock value
def test_read_sensor_without_init(self):
"""Test reading sensor without initialization."""
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
self.assertIsNone(accel)
def test_is_available_before_init(self):
"""Test is_available before initialization."""
self.assertFalse(SensorManager.is_available())
class TestSensorManagerWsenIsds(unittest.TestCase):
"""Test cases for SensorManager with WSEN_ISDS IMU."""
def setUp(self):
"""Set up test fixtures before each test."""
# Reset SensorManager singleton instance
SensorManager._instance = None
# Reset SensorManager class state
SensorManager._initialized = False
SensorManager._imu_driver = None
SensorManager._sensor_list = []
SensorManager._has_mcu_temperature = False
SensorManager._i2c_bus = None
SensorManager._i2c_address = None
# Create mock I2C bus with WSEN_ISDS
self.i2c_bus = MockI2C(0, sda=9, scl=18)
# Set WSEN_ISDS WHO_AM_I
self.i2c_bus.memory[0x6B] = {0x0F: [0x6A]}
def test_initialization_wsen_isds(self):
"""Test that SensorManager initializes with WSEN_ISDS."""
result = SensorManager.init(self.i2c_bus, address=0x6B)
self.assertTrue(result)
self.assertTrue(SensorManager.is_available())
def test_sensor_list_wsen_isds(self):
"""Test getting sensor list for WSEN_ISDS."""
SensorManager.init(self.i2c_bus, address=0x6B)
sensors = SensorManager.get_sensor_list()
# WSEN_ISDS provides: Accelerometer, Gyroscope, MCU Temperature
# (no IMU temperature)
self.assertGreaterEqual(len(sensors), 2)
# Check sensor types
sensor_types = [s.type for s in sensors]
self.assertIn(SensorManager.TYPE_ACCELEROMETER, sensor_types)
self.assertIn(SensorManager.TYPE_GYROSCOPE, sensor_types)
def test_read_accelerometer_wsen_isds(self):
"""Test reading accelerometer from WSEN_ISDS."""
SensorManager.init(self.i2c_bus, address=0x6B)
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
data = SensorManager.read_sensor(accel)
self.assertTrue(data is not None, f"read_sensor returned None, expected tuple")
self.assertEqual(len(data), 3)
ax, ay, az = data
# WSEN_ISDS mock returns 1000mg = 1G = 9.80665 m/s²
self.assertAlmostEqual(az, 9.80665, places=2)
class TestSensorManagerNoHardware(unittest.TestCase):
"""Test cases for SensorManager without hardware (desktop mode)."""
def setUp(self):
"""Set up test fixtures before each test."""
# Reset SensorManager singleton instance
SensorManager._instance = None
# Reset SensorManager class state
SensorManager._initialized = False
SensorManager._imu_driver = None
SensorManager._sensor_list = []
SensorManager._has_mcu_temperature = False
SensorManager._i2c_bus = None
SensorManager._i2c_address = None
# Create mock I2C bus with no devices
self.i2c_bus = MockI2C(0, sda=48, scl=47)
# No chip ID registered - simulates no hardware
def test_no_imu_detected(self):
"""Test behavior when no IMU is present."""
result = SensorManager.init(self.i2c_bus, address=0x6B)
# Returns True if MCU temp is available (even without IMU)
self.assertTrue(result)
def test_graceful_degradation(self):
"""Test graceful degradation when no sensors available."""
SensorManager.init(self.i2c_bus, address=0x6B)
# Should have at least MCU temperature
sensors = SensorManager.get_sensor_list()
self.assertGreaterEqual(len(sensors), 0)
# Reading non-existent sensor should return None
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
if accel is None:
# Expected when no IMU
pass
else:
# If somehow initialized, reading should handle gracefully
data = SensorManager.read_sensor(accel)
# Should either work or return None, not crash
self.assertTrue(data is None or len(data) == 3)
class TestSensorManagerMultipleInit(unittest.TestCase):
"""Test cases for multiple initialization calls."""
def setUp(self):
"""Set up test fixtures before each test."""
# Reset SensorManager singleton instance
SensorManager._instance = None
# Reset SensorManager class state
SensorManager._initialized = False
SensorManager._imu_driver = None
SensorManager._sensor_list = []
SensorManager._has_mcu_temperature = False
SensorManager._i2c_bus = None
SensorManager._i2c_address = None
# Create mock I2C bus with QMI8658
self.i2c_bus = MockI2C(0, sda=48, scl=47)
self.i2c_bus.memory[0x6B] = {_REG_PARTID: [_QMI8685_PARTID]}
def test_multiple_init_calls(self):
"""Test that multiple init calls are handled gracefully."""
result1 = SensorManager.init(self.i2c_bus, address=0x6B)
self.assertTrue(result1)
# Second init should return True but not re-initialize
result2 = SensorManager.init(self.i2c_bus, address=0x6B)
self.assertTrue(result2)
# Should still work normally
self.assertTrue(SensorManager.is_available())