You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
377 lines
13 KiB
Python
377 lines
13 KiB
Python
# Unit tests for SensorManager service
|
|
import unittest
|
|
import sys
|
|
|
|
|
|
# Mock hardware before importing SensorManager
|
|
class MockI2C:
|
|
"""Mock I2C bus for testing."""
|
|
def __init__(self, bus_id, sda=None, scl=None):
|
|
self.bus_id = bus_id
|
|
self.sda = sda
|
|
self.scl = scl
|
|
self.memory = {} # addr -> {reg -> value}
|
|
|
|
def readfrom_mem(self, addr, reg, nbytes):
|
|
"""Read from memory (simulates I2C read)."""
|
|
if addr not in self.memory:
|
|
raise OSError("I2C device not found")
|
|
if reg not in self.memory[addr]:
|
|
return bytes([0] * nbytes)
|
|
return bytes(self.memory[addr][reg])
|
|
|
|
def writeto_mem(self, addr, reg, data):
|
|
"""Write to memory (simulates I2C write)."""
|
|
if addr not in self.memory:
|
|
self.memory[addr] = {}
|
|
self.memory[addr][reg] = list(data)
|
|
|
|
|
|
class MockQMI8658:
|
|
"""Mock QMI8658 IMU sensor."""
|
|
def __init__(self, i2c_bus, address=0x6B, accel_scale=0b10, gyro_scale=0b100):
|
|
self.i2c = i2c_bus
|
|
self.address = address
|
|
self.accel_scale = accel_scale
|
|
self.gyro_scale = gyro_scale
|
|
|
|
@property
|
|
def temperature(self):
|
|
"""Return mock temperature."""
|
|
return 25.5 # Mock temperature in °C
|
|
|
|
@property
|
|
def acceleration(self):
|
|
"""Return mock acceleration (in G)."""
|
|
return (0.0, 0.0, 1.0) # At rest, Z-axis = 1G
|
|
|
|
@property
|
|
def gyro(self):
|
|
"""Return mock gyroscope (in deg/s)."""
|
|
return (0.0, 0.0, 0.0) # Stationary
|
|
|
|
|
|
class MockWsenIsds:
|
|
"""Mock WSEN_ISDS IMU sensor."""
|
|
def __init__(self, i2c, address=0x6B, acc_range="8g", acc_data_rate="104Hz",
|
|
gyro_range="500dps", gyro_data_rate="104Hz"):
|
|
self.i2c = i2c
|
|
self.address = address
|
|
self.acc_range = acc_range
|
|
self.gyro_range = gyro_range
|
|
self.acc_sensitivity = 0.244 # mg/digit for 8g
|
|
self.gyro_sensitivity = 17.5 # mdps/digit for 500dps
|
|
self.acc_offset_x = 0
|
|
self.acc_offset_y = 0
|
|
self.acc_offset_z = 0
|
|
self.gyro_offset_x = 0
|
|
self.gyro_offset_y = 0
|
|
self.gyro_offset_z = 0
|
|
|
|
def get_chip_id(self):
|
|
"""Return WHO_AM_I value."""
|
|
return 0x6A
|
|
|
|
def _read_raw_accelerations(self):
|
|
"""Return mock acceleration (in mg)."""
|
|
return (0.0, 0.0, 1000.0) # At rest, Z-axis = 1000 mg
|
|
|
|
def read_angular_velocities(self):
|
|
"""Return mock gyroscope (in mdps)."""
|
|
return (0.0, 0.0, 0.0)
|
|
|
|
def acc_calibrate(self, samples=None):
|
|
"""Mock calibration."""
|
|
pass
|
|
|
|
def gyro_calibrate(self, samples=None):
|
|
"""Mock calibration."""
|
|
pass
|
|
|
|
|
|
# Mock constants from drivers
|
|
_QMI8685_PARTID = 0x05
|
|
_REG_PARTID = 0x00
|
|
_ACCELSCALE_RANGE_8G = 0b10
|
|
_GYROSCALE_RANGE_256DPS = 0b100
|
|
|
|
|
|
# Create mock modules
|
|
mock_machine = type('module', (), {
|
|
'I2C': MockI2C,
|
|
'Pin': type('Pin', (), {})
|
|
})()
|
|
|
|
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
|
|
sys.modules['mpos.hardware.drivers.qmi8658'] = mock_qmi8658
|
|
sys.modules['mpos.hardware.drivers.wsen_isds'] = mock_wsen_isds
|
|
sys.modules['esp32'] = mock_esp32
|
|
|
|
# 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
|
|
import mpos.sensor_manager as SensorManager
|
|
|
|
|
|
class TestSensorManagerQMI8658(unittest.TestCase):
|
|
"""Test cases for SensorManager with QMI8658 IMU."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures before each test."""
|
|
# Reset SensorManager state
|
|
SensorManager._initialized = False
|
|
SensorManager._imu_driver = None
|
|
SensorManager._sensor_list = []
|
|
SensorManager._has_mcu_temperature = False
|
|
|
|
# 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 state
|
|
SensorManager._initialized = False
|
|
SensorManager._imu_driver = None
|
|
SensorManager._sensor_list = []
|
|
SensorManager._has_mcu_temperature = False
|
|
|
|
# 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 state
|
|
SensorManager._initialized = False
|
|
SensorManager._imu_driver = None
|
|
SensorManager._sensor_list = []
|
|
SensorManager._has_mcu_temperature = False
|
|
|
|
# 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 state
|
|
SensorManager._initialized = False
|
|
SensorManager._imu_driver = None
|
|
SensorManager._sensor_list = []
|
|
SensorManager._has_mcu_temperature = False
|
|
|
|
# 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())
|