#!/usr/bin/env python3 from collections import defaultdict, deque import pytest import time import numpy as np from dataclasses import dataclass from tabulate import tabulate import cereal.messaging as messaging from cereal.services import SERVICE_LIST from openpilot.common.mock import mock_messages from openpilot.selfdrive.car.car_helpers import write_car_param from openpilot.system.hardware.tici.power_monitor import get_power from openpilot.system.manager.process_config import managed_processes from openpilot.system.manager.manager import manager_cleanup SAMPLE_TIME = 8 # seconds to sample power MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples @dataclass class Proc: procs: list[str] power: float msgs: list[str] rtol: float = 0.05 atol: float = 0.12 @property def name(self): return '+'.join(self.procs) PROCS = [ Proc(['camerad'], 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), Proc(['modeld'], 1.12, atol=0.2, msgs=['modelV2']), Proc(['dmonitoringmodeld'], 0.4, msgs=['driverStateV2']), Proc(['encoderd'], 0.23, msgs=[]), ] @pytest.mark.tici class TestPowerDraw: def setup_method(self): write_car_param() # wait a bit for power save to disable time.sleep(5) def teardown_method(self): manager_cleanup() def get_expected_messages(self, proc): return int(sum(SAMPLE_TIME * SERVICE_LIST[msg].frequency for msg in proc.msgs)) def valid_msg_count(self, proc, msg_counts): msgs_received = sum(msg_counts[msg] for msg in proc.msgs) msgs_expected = self.get_expected_messages(proc) return np.core.numeric.isclose(msgs_expected, msgs_received, rtol=.02, atol=2) def valid_power_draw(self, proc, used): return np.core.numeric.isclose(used, proc.power, rtol=proc.rtol, atol=proc.atol) def tabulate_msg_counts(self, msgs_and_power): msg_counts = defaultdict(int) for _, counts in msgs_and_power: for msg, count in counts.items(): msg_counts[msg] += count return msg_counts def get_power_with_warmup_for_target(self, proc, prev): socks = {msg: messaging.sub_sock(msg) for msg in proc.msgs} for sock in socks.values(): messaging.drain_sock_raw(sock) msgs_and_power = deque([], maxlen=SAMPLE_TIME) start_time = time.monotonic() while (time.monotonic() - start_time) < MAX_WARMUP_TIME: power = get_power(1) iteration_msg_counts = {} for msg,sock in socks.items(): iteration_msg_counts[msg] = len(messaging.drain_sock_raw(sock)) msgs_and_power.append((power, iteration_msg_counts)) if len(msgs_and_power) < SAMPLE_TIME: continue msg_counts = self.tabulate_msg_counts(msgs_and_power) now = np.mean([m[0] for m in msgs_and_power]) if self.valid_msg_count(proc, msg_counts) and self.valid_power_draw(proc, now - prev): break return now, msg_counts, time.monotonic() - start_time - SAMPLE_TIME @mock_messages(['liveLocationKalman']) def test_camera_procs(self, subtests): baseline = get_power() prev = baseline used = {} warmup_time = {} msg_counts = {} for proc in PROCS: for p in proc.procs: managed_processes[p].start() now, local_msg_counts, warmup_time[proc.name] = self.get_power_with_warmup_for_target(proc, prev) msg_counts.update(local_msg_counts) used[proc.name] = now - prev prev = now manager_cleanup() tab = [['process', 'expected (W)', 'measured (W)', '# msgs expected', '# msgs received', "warmup time (s)"]] for proc in PROCS: cur = used[proc.name] expected = proc.power msgs_received = sum(msg_counts[msg] for msg in proc.msgs) tab.append([proc.name, round(expected, 2), round(cur, 2), self.get_expected_messages(proc), msgs_received, round(warmup_time[proc.name], 2)]) with subtests.test(proc=proc.name): assert self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs" assert self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W" print(tabulate(tab)) print(f"Baseline {baseline:.2f}W\n")