wip
This commit is contained in:
0
system/hardware/tici/tests/__init__.py
Normal file
0
system/hardware/tici/tests/__init__.py
Normal file
73
system/hardware/tici/tests/compare_casync_manifest.py
Normal file
73
system/hardware/tici/tests/compare_casync_manifest.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import collections
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
import openpilot.system.hardware.tici.casync as casync
|
||||
|
||||
|
||||
def get_chunk_download_size(chunk):
|
||||
sha = chunk.sha.hex()
|
||||
path = os.path.join(remote_url, sha[:4], sha + ".cacnk")
|
||||
if os.path.isfile(path):
|
||||
return os.path.getsize(path)
|
||||
else:
|
||||
r = requests.head(path, timeout=10)
|
||||
r.raise_for_status()
|
||||
return int(r.headers['content-length'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description='Compute overlap between two casync manifests')
|
||||
parser.add_argument('frm')
|
||||
parser.add_argument('to')
|
||||
args = parser.parse_args()
|
||||
|
||||
frm = casync.parse_caibx(args.frm)
|
||||
to = casync.parse_caibx(args.to)
|
||||
remote_url = args.to.replace('.caibx', '')
|
||||
|
||||
most_common = collections.Counter(t.sha for t in to).most_common(1)[0][0]
|
||||
|
||||
frm_dict = casync.build_chunk_dict(frm)
|
||||
|
||||
# Get content-length for each chunk
|
||||
with multiprocessing.Pool() as pool:
|
||||
szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to)))
|
||||
chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs, strict=True)}
|
||||
|
||||
sources: dict[str, list[int]] = {
|
||||
'seed': [],
|
||||
'remote_uncompressed': [],
|
||||
'remote_compressed': [],
|
||||
}
|
||||
|
||||
for chunk in to:
|
||||
# Assume most common chunk is the zero chunk
|
||||
if chunk.sha == most_common:
|
||||
continue
|
||||
|
||||
if chunk.sha in frm_dict:
|
||||
sources['seed'].append(chunk.length)
|
||||
else:
|
||||
sources['remote_uncompressed'].append(chunk.length)
|
||||
sources['remote_compressed'].append(chunk_sizes[chunk.sha])
|
||||
|
||||
print()
|
||||
print("Update statistics (excluding zeros)")
|
||||
print()
|
||||
print("Download only with no seed:")
|
||||
print(f" Remote (uncompressed)\t\t{sum(sources['seed'] + sources['remote_uncompressed']) / 1000 / 1000:.2f} MB\tn = {len(to)}")
|
||||
print(f" Remote (compressed download)\t{sum(chunk_sizes.values()) / 1000 / 1000:.2f} MB\tn = {len(to)}")
|
||||
print()
|
||||
print("Upgrade with seed partition:")
|
||||
print(f" Seed (uncompressed)\t\t{sum(sources['seed']) / 1000 / 1000:.2f} MB\t\t\t\tn = {len(sources['seed'])}")
|
||||
sz, n = sum(sources['remote_uncompressed']), len(sources['remote_uncompressed'])
|
||||
print(f" Remote (uncompressed)\t\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")
|
||||
sz, n = sum(sources['remote_compressed']), len(sources['remote_compressed'])
|
||||
print(f" Remote (compressed download)\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")
|
||||
26
system/hardware/tici/tests/test_agnos_updater.py
Normal file
26
system/hardware/tici/tests/test_agnos_updater.py
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
|
||||
|
||||
|
||||
class TestAgnosUpdater(unittest.TestCase):
|
||||
|
||||
def test_manifest(self):
|
||||
with open(MANIFEST) as f:
|
||||
m = json.load(f)
|
||||
|
||||
for img in m:
|
||||
r = requests.head(img['url'], timeout=10)
|
||||
r.raise_for_status()
|
||||
self.assertEqual(r.headers['Content-Type'], "application/x-xz")
|
||||
if not img['sparse']:
|
||||
assert img['hash'] == img['hash_raw']
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
75
system/hardware/tici/tests/test_amplifier.py
Normal file
75
system/hardware/tici/tests/test_amplifier.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
import random
|
||||
import unittest
|
||||
import subprocess
|
||||
|
||||
from panda import Panda
|
||||
from openpilot.system.hardware import TICI, HARDWARE
|
||||
from openpilot.system.hardware.tici.hardware import Tici
|
||||
from openpilot.system.hardware.tici.amplifier import Amplifier
|
||||
|
||||
|
||||
class TestAmplifier(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not TICI:
|
||||
raise unittest.SkipTest
|
||||
|
||||
def setUp(self):
|
||||
# clear dmesg
|
||||
subprocess.check_call("sudo dmesg -C", shell=True)
|
||||
|
||||
HARDWARE.reset_internal_panda()
|
||||
Panda.wait_for_panda(None, 30)
|
||||
self.panda = Panda()
|
||||
|
||||
def tearDown(self):
|
||||
HARDWARE.reset_internal_panda()
|
||||
|
||||
def _check_for_i2c_errors(self, expected):
|
||||
dmesg = subprocess.check_output("dmesg", shell=True, encoding='utf8')
|
||||
i2c_lines = [l for l in dmesg.strip().splitlines() if 'i2c_geni a88000.i2c' in l]
|
||||
i2c_str = '\n'.join(i2c_lines)
|
||||
|
||||
if not expected:
|
||||
return len(i2c_lines) == 0
|
||||
else:
|
||||
return "i2c error :-107" in i2c_str or "Bus arbitration lost" in i2c_str
|
||||
|
||||
def test_init(self):
|
||||
amp = Amplifier(debug=True)
|
||||
r = amp.initialize_configuration(Tici().get_device_type())
|
||||
assert r
|
||||
assert self._check_for_i2c_errors(False)
|
||||
|
||||
def test_shutdown(self):
|
||||
amp = Amplifier(debug=True)
|
||||
for _ in range(10):
|
||||
r = amp.set_global_shutdown(True)
|
||||
r = amp.set_global_shutdown(False)
|
||||
# amp config should be successful, with no i2c errors
|
||||
assert r
|
||||
assert self._check_for_i2c_errors(False)
|
||||
|
||||
def test_init_while_siren_play(self):
|
||||
for _ in range(10):
|
||||
self.panda.set_siren(False)
|
||||
time.sleep(0.1)
|
||||
|
||||
self.panda.set_siren(True)
|
||||
time.sleep(random.randint(0, 5))
|
||||
|
||||
amp = Amplifier(debug=True)
|
||||
r = amp.initialize_configuration(Tici().get_device_type())
|
||||
assert r
|
||||
|
||||
if self._check_for_i2c_errors(True):
|
||||
break
|
||||
else:
|
||||
self.fail("didn't hit any i2c errors")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
153
system/hardware/tici/tests/test_casync.py
Normal file
153
system/hardware/tici/tests/test_casync.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import unittest
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
import openpilot.system.hardware.tici.casync as casync
|
||||
|
||||
# dd if=/dev/zero of=/tmp/img.raw bs=1M count=2
|
||||
# sudo losetup -f /tmp/img.raw
|
||||
# losetup -a | grep img.raw
|
||||
LOOPBACK = os.environ.get('LOOPBACK', None)
|
||||
|
||||
|
||||
class TestCasync(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmpdir = tempfile.TemporaryDirectory()
|
||||
|
||||
# Build example contents
|
||||
chunk_a = [i % 256 for i in range(1024)] * 512
|
||||
chunk_b = [(256 - i) % 256 for i in range(1024)] * 512
|
||||
zeroes = [0] * (1024 * 128)
|
||||
contents = chunk_a + chunk_b + zeroes + chunk_a
|
||||
|
||||
cls.contents = bytes(contents)
|
||||
|
||||
# Write to file
|
||||
cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.bin')
|
||||
with open(cls.orig_fn, 'wb') as f:
|
||||
f.write(cls.contents)
|
||||
|
||||
# Create casync files
|
||||
cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx')
|
||||
cls.store_fn = os.path.join(cls.tmpdir.name, 'store')
|
||||
subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn])
|
||||
|
||||
target = casync.parse_caibx(cls.manifest_fn)
|
||||
hashes = [c.sha.hex() for c in target]
|
||||
|
||||
# Ensure we have chunk reuse
|
||||
assert len(hashes) > len(set(hashes))
|
||||
|
||||
def setUp(self):
|
||||
# Clear target_lo
|
||||
if LOOPBACK is not None:
|
||||
self.target_lo = LOOPBACK
|
||||
with open(self.target_lo, 'wb') as f:
|
||||
f.write(b"0" * len(self.contents))
|
||||
|
||||
self.target_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
|
||||
self.seed_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
|
||||
|
||||
def tearDown(self):
|
||||
for fn in [self.target_fn, self.seed_fn]:
|
||||
try:
|
||||
os.unlink(fn)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def test_simple_extract(self):
|
||||
target = casync.parse_caibx(self.manifest_fn)
|
||||
|
||||
sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
|
||||
stats = casync.extract(target, sources, self.target_fn)
|
||||
|
||||
with open(self.target_fn, 'rb') as target_f:
|
||||
self.assertEqual(target_f.read(), self.contents)
|
||||
|
||||
self.assertEqual(stats['remote'], len(self.contents))
|
||||
|
||||
def test_seed(self):
|
||||
target = casync.parse_caibx(self.manifest_fn)
|
||||
|
||||
# Populate seed with half of the target contents
|
||||
with open(self.seed_fn, 'wb') as seed_f:
|
||||
seed_f.write(self.contents[:len(self.contents) // 2])
|
||||
|
||||
sources = [('seed', casync.FileChunkReader(self.seed_fn), casync.build_chunk_dict(target))]
|
||||
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
|
||||
stats = casync.extract(target, sources, self.target_fn)
|
||||
|
||||
with open(self.target_fn, 'rb') as target_f:
|
||||
self.assertEqual(target_f.read(), self.contents)
|
||||
|
||||
self.assertGreater(stats['seed'], 0)
|
||||
self.assertLess(stats['remote'], len(self.contents))
|
||||
|
||||
def test_already_done(self):
|
||||
"""Test that an already flashed target doesn't download any chunks"""
|
||||
target = casync.parse_caibx(self.manifest_fn)
|
||||
|
||||
with open(self.target_fn, 'wb') as f:
|
||||
f.write(self.contents)
|
||||
|
||||
sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
|
||||
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
|
||||
|
||||
stats = casync.extract(target, sources, self.target_fn)
|
||||
|
||||
with open(self.target_fn, 'rb') as f:
|
||||
self.assertEqual(f.read(), self.contents)
|
||||
|
||||
self.assertEqual(stats['target'], len(self.contents))
|
||||
|
||||
def test_chunk_reuse(self):
|
||||
"""Test that chunks that are reused are only downloaded once"""
|
||||
target = casync.parse_caibx(self.manifest_fn)
|
||||
|
||||
# Ensure target exists
|
||||
with open(self.target_fn, 'wb'):
|
||||
pass
|
||||
|
||||
sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
|
||||
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
|
||||
|
||||
stats = casync.extract(target, sources, self.target_fn)
|
||||
|
||||
with open(self.target_fn, 'rb') as f:
|
||||
self.assertEqual(f.read(), self.contents)
|
||||
|
||||
self.assertLess(stats['remote'], len(self.contents))
|
||||
|
||||
@unittest.skipUnless(LOOPBACK, "requires loopback device")
|
||||
def test_lo_simple_extract(self):
|
||||
target = casync.parse_caibx(self.manifest_fn)
|
||||
sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
|
||||
|
||||
stats = casync.extract(target, sources, self.target_lo)
|
||||
|
||||
with open(self.target_lo, 'rb') as target_f:
|
||||
self.assertEqual(target_f.read(len(self.contents)), self.contents)
|
||||
|
||||
self.assertEqual(stats['remote'], len(self.contents))
|
||||
|
||||
@unittest.skipUnless(LOOPBACK, "requires loopback device")
|
||||
def test_lo_chunk_reuse(self):
|
||||
"""Test that chunks that are reused are only downloaded once"""
|
||||
target = casync.parse_caibx(self.manifest_fn)
|
||||
|
||||
sources = [('target', casync.FileChunkReader(self.target_lo), casync.build_chunk_dict(target))]
|
||||
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
|
||||
|
||||
stats = casync.extract(target, sources, self.target_lo)
|
||||
|
||||
with open(self.target_lo, 'rb') as f:
|
||||
self.assertEqual(f.read(len(self.contents)), self.contents)
|
||||
|
||||
self.assertLess(stats['remote'], len(self.contents))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
28
system/hardware/tici/tests/test_hardware.py
Normal file
28
system/hardware/tici/tests/test_hardware.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
import time
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
from openpilot.system.hardware.tici.hardware import Tici
|
||||
|
||||
HARDWARE = Tici()
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestHardware(unittest.TestCase):
|
||||
|
||||
def test_power_save_time(self):
|
||||
ts = []
|
||||
for _ in range(5):
|
||||
for on in (True, False):
|
||||
st = time.monotonic()
|
||||
HARDWARE.set_power_save(on)
|
||||
ts.append(time.monotonic() - st)
|
||||
|
||||
assert 0.1 < np.mean(ts) < 0.25
|
||||
assert max(ts) < 0.3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
134
system/hardware/tici/tests/test_power_draw.py
Normal file
134
system/hardware/tici/tests/test_power_draw.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
from collections import defaultdict, deque
|
||||
import pytest
|
||||
import unittest
|
||||
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.selfdrive.manager.process_config import managed_processes
|
||||
from openpilot.selfdrive.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=[]),
|
||||
Proc(['mapsd', 'navmodeld'], 0.05, msgs=['mapRenderState', 'navModel']),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPowerDraw(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
write_car_param()
|
||||
|
||||
# wait a bit for power save to disable
|
||||
time.sleep(5)
|
||||
|
||||
def tearDown(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):
|
||||
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 self.subTest(proc=proc.name):
|
||||
self.assertTrue(self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs")
|
||||
self.assertTrue(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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user