Skip to content

Commit eaa2ee3

Browse files
Add tests/test_sensor_manager.py
1 parent ce981d7 commit eaa2ee3

File tree

1 file changed

+376
-0
lines changed

1 file changed

+376
-0
lines changed

tests/test_sensor_manager.py

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
# Unit tests for SensorManager service
2+
import unittest
3+
import sys
4+
5+
6+
# Mock hardware before importing SensorManager
7+
class MockI2C:
8+
"""Mock I2C bus for testing."""
9+
def __init__(self, bus_id, sda=None, scl=None):
10+
self.bus_id = bus_id
11+
self.sda = sda
12+
self.scl = scl
13+
self.memory = {} # addr -> {reg -> value}
14+
15+
def readfrom_mem(self, addr, reg, nbytes):
16+
"""Read from memory (simulates I2C read)."""
17+
if addr not in self.memory:
18+
raise OSError("I2C device not found")
19+
if reg not in self.memory[addr]:
20+
return bytes([0] * nbytes)
21+
return bytes(self.memory[addr][reg])
22+
23+
def writeto_mem(self, addr, reg, data):
24+
"""Write to memory (simulates I2C write)."""
25+
if addr not in self.memory:
26+
self.memory[addr] = {}
27+
self.memory[addr][reg] = list(data)
28+
29+
30+
class MockQMI8658:
31+
"""Mock QMI8658 IMU sensor."""
32+
def __init__(self, i2c_bus, address=0x6B, accel_scale=0b10, gyro_scale=0b100):
33+
self.i2c = i2c_bus
34+
self.address = address
35+
self.accel_scale = accel_scale
36+
self.gyro_scale = gyro_scale
37+
38+
@property
39+
def temperature(self):
40+
"""Return mock temperature."""
41+
return 25.5 # Mock temperature in °C
42+
43+
@property
44+
def acceleration(self):
45+
"""Return mock acceleration (in G)."""
46+
return (0.0, 0.0, 1.0) # At rest, Z-axis = 1G
47+
48+
@property
49+
def gyro(self):
50+
"""Return mock gyroscope (in deg/s)."""
51+
return (0.0, 0.0, 0.0) # Stationary
52+
53+
54+
class MockWsenIsds:
55+
"""Mock WSEN_ISDS IMU sensor."""
56+
def __init__(self, i2c, address=0x6B, acc_range="8g", acc_data_rate="104Hz",
57+
gyro_range="500dps", gyro_data_rate="104Hz"):
58+
self.i2c = i2c
59+
self.address = address
60+
self.acc_range = acc_range
61+
self.gyro_range = gyro_range
62+
self.acc_sensitivity = 0.244 # mg/digit for 8g
63+
self.gyro_sensitivity = 17.5 # mdps/digit for 500dps
64+
self.acc_offset_x = 0
65+
self.acc_offset_y = 0
66+
self.acc_offset_z = 0
67+
self.gyro_offset_x = 0
68+
self.gyro_offset_y = 0
69+
self.gyro_offset_z = 0
70+
71+
def get_chip_id(self):
72+
"""Return WHO_AM_I value."""
73+
return 0x6A
74+
75+
def read_accelerations(self):
76+
"""Return mock acceleration (in mg)."""
77+
return (0.0, 0.0, 1000.0) # At rest, Z-axis = 1000 mg
78+
79+
def read_angular_velocities(self):
80+
"""Return mock gyroscope (in mdps)."""
81+
return (0.0, 0.0, 0.0)
82+
83+
def acc_calibrate(self, samples=None):
84+
"""Mock calibration."""
85+
pass
86+
87+
def gyro_calibrate(self, samples=None):
88+
"""Mock calibration."""
89+
pass
90+
91+
92+
# Mock constants from drivers
93+
_QMI8685_PARTID = 0x05
94+
_REG_PARTID = 0x00
95+
_ACCELSCALE_RANGE_8G = 0b10
96+
_GYROSCALE_RANGE_256DPS = 0b100
97+
98+
99+
# Create mock modules
100+
mock_machine = type('module', (), {
101+
'I2C': MockI2C,
102+
'Pin': type('Pin', (), {})
103+
})()
104+
105+
mock_qmi8658 = type('module', (), {
106+
'QMI8658': MockQMI8658,
107+
'_QMI8685_PARTID': _QMI8685_PARTID,
108+
'_REG_PARTID': _REG_PARTID,
109+
'_ACCELSCALE_RANGE_8G': _ACCELSCALE_RANGE_8G,
110+
'_GYROSCALE_RANGE_256DPS': _GYROSCALE_RANGE_256DPS
111+
})()
112+
113+
mock_wsen_isds = type('module', (), {
114+
'Wsen_Isds': MockWsenIsds
115+
})()
116+
117+
# Mock esp32 module
118+
def _mock_mcu_temperature(*args, **kwargs):
119+
"""Mock MCU temperature sensor."""
120+
return 42.0
121+
122+
mock_esp32 = type('module', (), {
123+
'mcu_temperature': _mock_mcu_temperature
124+
})()
125+
126+
# Inject mocks into sys.modules
127+
sys.modules['machine'] = mock_machine
128+
sys.modules['mpos.hardware.drivers.qmi8658'] = mock_qmi8658
129+
sys.modules['mpos.hardware.drivers.wsen_isds'] = mock_wsen_isds
130+
sys.modules['esp32'] = mock_esp32
131+
132+
# Mock _thread for thread safety testing
133+
try:
134+
import _thread
135+
except ImportError:
136+
mock_thread = type('module', (), {
137+
'allocate_lock': lambda: type('lock', (), {
138+
'acquire': lambda self: None,
139+
'release': lambda self: None
140+
})()
141+
})()
142+
sys.modules['_thread'] = mock_thread
143+
144+
# Now import the module to test
145+
import mpos.sensor_manager as SensorManager
146+
147+
148+
class TestSensorManagerQMI8658(unittest.TestCase):
149+
"""Test cases for SensorManager with QMI8658 IMU."""
150+
151+
def setUp(self):
152+
"""Set up test fixtures before each test."""
153+
# Reset SensorManager state
154+
SensorManager._initialized = False
155+
SensorManager._imu_driver = None
156+
SensorManager._sensor_list = []
157+
SensorManager._has_mcu_temperature = False
158+
159+
# Create mock I2C bus with QMI8658
160+
self.i2c_bus = MockI2C(0, sda=48, scl=47)
161+
# Set QMI8658 chip ID
162+
self.i2c_bus.memory[0x6B] = {_REG_PARTID: [_QMI8685_PARTID]}
163+
164+
def test_initialization_qmi8658(self):
165+
"""Test that SensorManager initializes with QMI8658."""
166+
result = SensorManager.init(self.i2c_bus, address=0x6B)
167+
self.assertTrue(result)
168+
self.assertTrue(SensorManager.is_available())
169+
170+
def test_sensor_list_qmi8658(self):
171+
"""Test getting sensor list for QMI8658."""
172+
SensorManager.init(self.i2c_bus, address=0x6B)
173+
sensors = SensorManager.get_sensor_list()
174+
175+
# QMI8658 provides: Accelerometer, Gyroscope, IMU Temperature, MCU Temperature
176+
self.assertGreaterEqual(len(sensors), 3)
177+
178+
# Check sensor types present
179+
sensor_types = [s.type for s in sensors]
180+
self.assertIn(SensorManager.TYPE_ACCELEROMETER, sensor_types)
181+
self.assertIn(SensorManager.TYPE_GYROSCOPE, sensor_types)
182+
self.assertIn(SensorManager.TYPE_IMU_TEMPERATURE, sensor_types)
183+
184+
def test_get_default_sensor(self):
185+
"""Test getting default sensor by type."""
186+
SensorManager.init(self.i2c_bus, address=0x6B)
187+
188+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
189+
self.assertIsNotNone(accel)
190+
self.assertEqual(accel.type, SensorManager.TYPE_ACCELEROMETER)
191+
192+
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
193+
self.assertIsNotNone(gyro)
194+
self.assertEqual(gyro.type, SensorManager.TYPE_GYROSCOPE)
195+
196+
def test_get_nonexistent_sensor(self):
197+
"""Test getting a sensor type that doesn't exist."""
198+
SensorManager.init(self.i2c_bus, address=0x6B)
199+
200+
# Type 999 doesn't exist
201+
sensor = SensorManager.get_default_sensor(999)
202+
self.assertIsNone(sensor)
203+
204+
def test_read_accelerometer(self):
205+
"""Test reading accelerometer data."""
206+
SensorManager.init(self.i2c_bus, address=0x6B)
207+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
208+
209+
data = SensorManager.read_sensor(accel)
210+
self.assertTrue(data is not None, f"read_sensor returned None, expected tuple")
211+
self.assertEqual(len(data), 3) # (x, y, z)
212+
213+
ax, ay, az = data
214+
# At rest, Z should be ~9.8 m/s² (1G converted to m/s²)
215+
self.assertAlmostEqual(az, 9.80665, places=2)
216+
217+
def test_read_gyroscope(self):
218+
"""Test reading gyroscope data."""
219+
SensorManager.init(self.i2c_bus, address=0x6B)
220+
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
221+
222+
data = SensorManager.read_sensor(gyro)
223+
self.assertTrue(data is not None, f"read_sensor returned None, expected tuple")
224+
self.assertEqual(len(data), 3) # (x, y, z)
225+
226+
gx, gy, gz = data
227+
# Stationary, all should be ~0 deg/s
228+
self.assertAlmostEqual(gx, 0.0, places=1)
229+
self.assertAlmostEqual(gy, 0.0, places=1)
230+
self.assertAlmostEqual(gz, 0.0, places=1)
231+
232+
def test_read_temperature(self):
233+
"""Test reading temperature data."""
234+
SensorManager.init(self.i2c_bus, address=0x6B)
235+
236+
# Try IMU temperature
237+
imu_temp = SensorManager.get_default_sensor(SensorManager.TYPE_IMU_TEMPERATURE)
238+
if imu_temp:
239+
temp = SensorManager.read_sensor(imu_temp)
240+
self.assertIsNotNone(temp)
241+
self.assertIsInstance(temp, (int, float))
242+
243+
# Try MCU temperature
244+
mcu_temp = SensorManager.get_default_sensor(SensorManager.TYPE_SOC_TEMPERATURE)
245+
if mcu_temp:
246+
temp = SensorManager.read_sensor(mcu_temp)
247+
self.assertIsNotNone(temp)
248+
self.assertEqual(temp, 42.0) # Mock value
249+
250+
def test_read_sensor_without_init(self):
251+
"""Test reading sensor without initialization."""
252+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
253+
self.assertIsNone(accel)
254+
255+
def test_is_available_before_init(self):
256+
"""Test is_available before initialization."""
257+
self.assertFalse(SensorManager.is_available())
258+
259+
260+
class TestSensorManagerWsenIsds(unittest.TestCase):
261+
"""Test cases for SensorManager with WSEN_ISDS IMU."""
262+
263+
def setUp(self):
264+
"""Set up test fixtures before each test."""
265+
# Reset SensorManager state
266+
SensorManager._initialized = False
267+
SensorManager._imu_driver = None
268+
SensorManager._sensor_list = []
269+
SensorManager._has_mcu_temperature = False
270+
271+
# Create mock I2C bus with WSEN_ISDS
272+
self.i2c_bus = MockI2C(0, sda=9, scl=18)
273+
# Set WSEN_ISDS WHO_AM_I
274+
self.i2c_bus.memory[0x6B] = {0x0F: [0x6A]}
275+
276+
def test_initialization_wsen_isds(self):
277+
"""Test that SensorManager initializes with WSEN_ISDS."""
278+
result = SensorManager.init(self.i2c_bus, address=0x6B)
279+
self.assertTrue(result)
280+
self.assertTrue(SensorManager.is_available())
281+
282+
def test_sensor_list_wsen_isds(self):
283+
"""Test getting sensor list for WSEN_ISDS."""
284+
SensorManager.init(self.i2c_bus, address=0x6B)
285+
sensors = SensorManager.get_sensor_list()
286+
287+
# WSEN_ISDS provides: Accelerometer, Gyroscope, MCU Temperature
288+
# (no IMU temperature)
289+
self.assertGreaterEqual(len(sensors), 2)
290+
291+
# Check sensor types
292+
sensor_types = [s.type for s in sensors]
293+
self.assertIn(SensorManager.TYPE_ACCELEROMETER, sensor_types)
294+
self.assertIn(SensorManager.TYPE_GYROSCOPE, sensor_types)
295+
296+
def test_read_accelerometer_wsen_isds(self):
297+
"""Test reading accelerometer from WSEN_ISDS."""
298+
SensorManager.init(self.i2c_bus, address=0x6B)
299+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
300+
301+
data = SensorManager.read_sensor(accel)
302+
self.assertTrue(data is not None, f"read_sensor returned None, expected tuple")
303+
self.assertEqual(len(data), 3)
304+
305+
ax, ay, az = data
306+
# WSEN_ISDS mock returns 1000mg = 1G = 9.80665 m/s²
307+
self.assertAlmostEqual(az, 9.80665, places=2)
308+
309+
310+
class TestSensorManagerNoHardware(unittest.TestCase):
311+
"""Test cases for SensorManager without hardware (desktop mode)."""
312+
313+
def setUp(self):
314+
"""Set up test fixtures before each test."""
315+
# Reset SensorManager state
316+
SensorManager._initialized = False
317+
SensorManager._imu_driver = None
318+
SensorManager._sensor_list = []
319+
SensorManager._has_mcu_temperature = False
320+
321+
# Create mock I2C bus with no devices
322+
self.i2c_bus = MockI2C(0, sda=48, scl=47)
323+
# No chip ID registered - simulates no hardware
324+
325+
def test_no_imu_detected(self):
326+
"""Test behavior when no IMU is present."""
327+
result = SensorManager.init(self.i2c_bus, address=0x6B)
328+
# Returns True if MCU temp is available (even without IMU)
329+
self.assertTrue(result)
330+
331+
def test_graceful_degradation(self):
332+
"""Test graceful degradation when no sensors available."""
333+
SensorManager.init(self.i2c_bus, address=0x6B)
334+
335+
# Should have at least MCU temperature
336+
sensors = SensorManager.get_sensor_list()
337+
self.assertGreaterEqual(len(sensors), 0)
338+
339+
# Reading non-existent sensor should return None
340+
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
341+
if accel is None:
342+
# Expected when no IMU
343+
pass
344+
else:
345+
# If somehow initialized, reading should handle gracefully
346+
data = SensorManager.read_sensor(accel)
347+
# Should either work or return None, not crash
348+
self.assertTrue(data is None or len(data) == 3)
349+
350+
351+
class TestSensorManagerMultipleInit(unittest.TestCase):
352+
"""Test cases for multiple initialization calls."""
353+
354+
def setUp(self):
355+
"""Set up test fixtures before each test."""
356+
# Reset SensorManager state
357+
SensorManager._initialized = False
358+
SensorManager._imu_driver = None
359+
SensorManager._sensor_list = []
360+
SensorManager._has_mcu_temperature = False
361+
362+
# Create mock I2C bus with QMI8658
363+
self.i2c_bus = MockI2C(0, sda=48, scl=47)
364+
self.i2c_bus.memory[0x6B] = {_REG_PARTID: [_QMI8685_PARTID]}
365+
366+
def test_multiple_init_calls(self):
367+
"""Test that multiple init calls are handled gracefully."""
368+
result1 = SensorManager.init(self.i2c_bus, address=0x6B)
369+
self.assertTrue(result1)
370+
371+
# Second init should return True but not re-initialize
372+
result2 = SensorManager.init(self.i2c_bus, address=0x6B)
373+
self.assertTrue(result2)
374+
375+
# Should still work normally
376+
self.assertTrue(SensorManager.is_available())

0 commit comments

Comments
 (0)