#!/usr/bin/env python3 import os import time import unittest import numpy as np from collections import namedtuple, defaultdict import cereal.messaging as messaging from cereal import log from common.gpio import get_irqs_for_action from system.hardware import TICI from selfdrive.manager.process_config import managed_processes BMX = { ('bmx055', 'acceleration'), ('bmx055', 'gyroUncalibrated'), ('bmx055', 'magneticUncalibrated'), ('bmx055', 'temperature'), } LSM = { ('lsm6ds3', 'acceleration'), ('lsm6ds3', 'gyroUncalibrated'), ('lsm6ds3', 'temperature'), } LSM_C = {(x[0]+'trc', x[1]) for x in LSM} MMC = { ('mmc5603nj', 'magneticUncalibrated'), } RPR = { ('rpr0521', 'light'), } SENSOR_CONFIGURATIONS = ( (BMX | LSM | RPR), (MMC | LSM | RPR), (BMX | LSM_C | RPR), (MMC| LSM_C | RPR), ) Sensor = log.SensorEventData.SensorSource SensorConfig = namedtuple('SensorConfig', ['type', 'sanity_min', 'sanity_max', 'expected_freq']) ALL_SENSORS = { Sensor.rpr0521: { SensorConfig("light", 0, 1023, 100), }, Sensor.lsm6ds3: { SensorConfig("acceleration", 5, 15, 100), SensorConfig("gyroUncalibrated", 0, .2, 100), SensorConfig("temperature", 0, 60, 100), }, Sensor.lsm6ds3trc: { SensorConfig("acceleration", 5, 15, 104), SensorConfig("gyroUncalibrated", 0, .2, 104), SensorConfig("temperature", 0, 60, 100), }, Sensor.bmx055: { SensorConfig("acceleration", 5, 15, 100), SensorConfig("gyroUncalibrated", 0, .2, 100), SensorConfig("magneticUncalibrated", 0, 300, 100), SensorConfig("temperature", 0, 60, 100), }, Sensor.mmc5603nj: { SensorConfig("magneticUncalibrated", 0, 300, 100), } } def get_irq_count(irq: int): with open(f"/sys/kernel/irq/{irq}/per_cpu_count") as f: per_cpu = map(int, f.read().split(",")) return sum(per_cpu) def read_sensor_events(duration_sec): sensor_types = ['accelerometer', 'gyroscope', 'magnetometer', 'accelerometer2', 'gyroscope2', 'lightSensor', 'temperatureSensor'] esocks = {} events = defaultdict(list) for stype in sensor_types: esocks[stype] = messaging.sub_sock(stype, timeout=0.1) start_time_sec = time.monotonic() while time.monotonic() - start_time_sec < duration_sec: for esock in esocks: events[esock] += messaging.drain_sock(esocks[esock]) time.sleep(0.1) assert sum(map(len, events.values())) != 0, "No sensor events collected!" return events class TestSensord(unittest.TestCase): @classmethod def setUpClass(cls): if not TICI: raise unittest.SkipTest # enable LSM self test os.environ["LSM_SELF_TEST"] = "1" # read initial sensor values every test case can use os.system("pkill -f ./_sensord") try: managed_processes["sensord"].start() time.sleep(3) cls.sample_secs = 10 cls.events = read_sensor_events(cls.sample_secs) # determine sensord's irq cls.sensord_irq = get_irqs_for_action("sensord")[0] finally: # teardown won't run if this doesn't succeed managed_processes["sensord"].stop() @classmethod def tearDownClass(cls): managed_processes["sensord"].stop() def tearDown(self): managed_processes["sensord"].stop() def test_sensors_present(self): # verify correct sensors configuration seen = set() for etype in self.events: for measurement in self.events[etype]: m = getattr(measurement, measurement.which()) seen.add((str(m.source), m.which())) self.assertIn(seen, SENSOR_CONFIGURATIONS) def test_lsm6ds3_timing(self): # verify measurements are sampled and published at 104Hz sensor_t = { 1: [], # accel 5: [], # gyro } for measurement in self.events['accelerometer']: m = getattr(measurement, measurement.which()) sensor_t[m.sensor].append(m.timestamp) for measurement in self.events['gyroscope']: m = getattr(measurement, measurement.which()) sensor_t[m.sensor].append(m.timestamp) for s, vals in sensor_t.items(): with self.subTest(sensor=s): assert len(vals) > 0 tdiffs = np.diff(vals) / 1e6 # millis high_delay_diffs = list(filter(lambda d: d >= 20., tdiffs)) assert len(high_delay_diffs) < 15, f"Too many large diffs: {high_delay_diffs}" avg_diff = sum(tdiffs)/len(tdiffs) avg_freq = 1. / (avg_diff * 1e-3) assert 92. < avg_freq < 114., f"avg freq {avg_freq}Hz wrong, expected 104Hz" stddev = np.std(tdiffs) assert stddev < 2.0, f"Standard-dev to big {stddev}" def test_events_check(self): # verify if all sensors produce events sensor_events = dict() for etype in self.events: for measurement in self.events[etype]: m = getattr(measurement, measurement.which()) if m.type in sensor_events: sensor_events[m.type] += 1 else: sensor_events[m.type] = 1 for s in sensor_events: err_msg = f"Sensor {s}: 200 < {sensor_events[s]}" assert sensor_events[s] > 200, err_msg def test_logmonottime_timestamp_diff(self): # ensure diff between the message logMonotime and sample timestamp is small tdiffs = list() for etype in self.events: for measurement in self.events[etype]: m = getattr(measurement, measurement.which()) # check if gyro and accel timestamps are before logMonoTime if str(m.source).startswith("lsm6ds3") and m.which() != 'temperature': err_msg = f"Timestamp after logMonoTime: {m.timestamp} > {measurement.logMonoTime}" assert m.timestamp < measurement.logMonoTime, err_msg # negative values might occur, as non interrupt packages created # before the sensor is read tdiffs.append(abs(measurement.logMonoTime - m.timestamp) / 1e6) high_delay_diffs = set(filter(lambda d: d >= 15., tdiffs)) assert len(high_delay_diffs) < 20, f"Too many measurements published : {high_delay_diffs}" avg_diff = round(sum(tdiffs)/len(tdiffs), 4) assert avg_diff < 4, f"Avg packet diff: {avg_diff:.1f}ms" stddev = np.std(tdiffs) assert stddev < 2, f"Timing diffs have too high stddev: {stddev}" def test_sensor_values_sanity_check(self): sensor_values = dict() for etype in self.events: for measurement in self.events[etype]: m = getattr(measurement, measurement.which()) key = (m.source.raw, m.which()) values = getattr(m, m.which()) if hasattr(values, 'v'): values = values.v values = np.atleast_1d(values) if key in sensor_values: sensor_values[key].append(values) else: sensor_values[key] = [values] # Sanity check sensor values and counts for sensor, stype in sensor_values: for s in ALL_SENSORS[sensor]: if s.type != stype: continue key = (sensor, s.type) val_cnt = len(sensor_values[key]) min_samples = self.sample_secs * s.expected_freq err_msg = f"Sensor {sensor} {s.type} got {val_cnt} measurements, expected {min_samples}" assert min_samples*0.9 < val_cnt < min_samples*1.1, err_msg mean_norm = np.mean(np.linalg.norm(sensor_values[key], axis=1)) err_msg = f"Sensor '{sensor} {s.type}' failed sanity checks {mean_norm} is not between {s.sanity_min} and {s.sanity_max}" assert s.sanity_min <= mean_norm <= s.sanity_max, err_msg def test_sensor_verify_no_interrupts_after_stop(self): managed_processes["sensord"].start() time.sleep(3) # read /proc/interrupts to verify interrupts are received state_one = get_irq_count(self.sensord_irq) time.sleep(1) state_two = get_irq_count(self.sensord_irq) error_msg = f"no interrupts received after sensord start!\n{state_one} {state_two}" assert state_one != state_two, error_msg managed_processes["sensord"].stop() time.sleep(1) # read /proc/interrupts to verify no more interrupts are received state_one = get_irq_count(self.sensord_irq) time.sleep(1) state_two = get_irq_count(self.sensord_irq) assert state_one == state_two, "Interrupts received after sensord stop!" if __name__ == "__main__": unittest.main()