IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
0
iqpilot/system/hardware/tici/tests/__init__.py
Normal file
0
iqpilot/system/hardware/tici/tests/__init__.py
Normal file
73
iqpilot/system/hardware/tici/tests/compare_casync_manifest.py
Executable file
73
iqpilot/system/hardware/tici/tests/compare_casync_manifest.py
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import collections
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
import iqpilot.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}")
|
||||
37
iqpilot/system/hardware/tici/tests/test_agnos_updater.py
Normal file
37
iqpilot/system/hardware/tici/tests/test_agnos_updater.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
|
||||
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
MANIFESTS = [
|
||||
os.path.join(TEST_DIR, "../agnos.json"),
|
||||
os.path.join(TEST_DIR, "../agnos_tici_15_1.json"),
|
||||
]
|
||||
|
||||
IMAGE_HOST = "git.konn3kt.com"
|
||||
|
||||
XZ_MAGIC = b"\xfd7zXZ\x00"
|
||||
LFS_POINTER_MAGIC = b"version https://git-lfs"
|
||||
|
||||
|
||||
class TestAgnosUpdater:
|
||||
|
||||
def test_manifest(self):
|
||||
for manifest in MANIFESTS:
|
||||
with open(manifest) as f:
|
||||
m = json.load(f)
|
||||
|
||||
for img in m:
|
||||
assert img['url'].split('/')[2] == IMAGE_HOST
|
||||
if not img['sparse']:
|
||||
assert img['hash'] == img['hash_raw']
|
||||
|
||||
s = requests.Session()
|
||||
s.trust_env = False
|
||||
r = s.get(img['url'], timeout=10, stream=True,
|
||||
headers={"User-Agent": "IQOS-Updater"})
|
||||
if r.status_code in (401, 403, 404):
|
||||
continue
|
||||
head = next(r.iter_content(chunk_size=256), b"") or b""
|
||||
assert not head.startswith(XZ_MAGIC), f"{img['name']}: anonymous request served image content"
|
||||
assert not head.startswith(LFS_POINTER_MAGIC), f"{img['name']}: anonymous request served the LFS pointer"
|
||||
66
iqpilot/system/hardware/tici/tests/test_amplifier.py
Normal file
66
iqpilot/system/hardware/tici/tests/test_amplifier.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import pytest
|
||||
import time
|
||||
import random
|
||||
import subprocess
|
||||
|
||||
from panda import Panda
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.hardware.tici.hardware import Tici
|
||||
from iqpilot.system.hardware.tici.amplifier import Amplifier
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestAmplifier:
|
||||
|
||||
def setup_method(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_method(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:
|
||||
pytest.fail("didn't hit any i2c errors")
|
||||
238
iqpilot/system/hardware/tici/tests/test_esim.py
Normal file
238
iqpilot/system/hardware/tici/tests/test_esim.py
Normal file
@@ -0,0 +1,238 @@
|
||||
import pytest
|
||||
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.hardware.base import LPAError, LPAProfileNotFoundError, Profile
|
||||
from iqpilot.system.hardware.tici import lpa as lpa_module
|
||||
from iqpilot.system.hardware.tici.esim_manager import EsimManager
|
||||
|
||||
# https://euicc-manual.osmocom.org/docs/rsp/known-test-profile
|
||||
# iccid is always the same for the given activation code
|
||||
TEST_ACTIVATION_CODE = 'LPA:1$rsp.truphone.com$QRF-BETTERROAMING-PMRDGIR2EARDEIT5'
|
||||
TEST_ICCID = '8944476500001944011'
|
||||
|
||||
TEST_NICKNAME = 'test_profile'
|
||||
|
||||
def cleanup():
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
try:
|
||||
lpa.delete_profile(TEST_ICCID)
|
||||
except LPAProfileNotFoundError:
|
||||
pass
|
||||
lpa.process_notifications()
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestEsim:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cleanup()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
cleanup()
|
||||
|
||||
def test_provision_enable_disable(self):
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
current_active = lpa.get_active_profile()
|
||||
|
||||
lpa.download_profile(TEST_ACTIVATION_CODE, TEST_NICKNAME)
|
||||
assert any(p.iccid == TEST_ICCID and p.nickname == TEST_NICKNAME for p in lpa.list_profiles())
|
||||
|
||||
lpa.enable_profile(TEST_ICCID)
|
||||
new_active = lpa.get_active_profile()
|
||||
assert new_active is not None
|
||||
assert new_active.iccid == TEST_ICCID
|
||||
assert new_active.nickname == TEST_NICKNAME
|
||||
|
||||
lpa.disable_profile(TEST_ICCID)
|
||||
new_active = lpa.get_active_profile()
|
||||
assert new_active is None
|
||||
|
||||
if current_active:
|
||||
lpa.enable_profile(current_active.iccid)
|
||||
|
||||
|
||||
class TestEsimDeleteHandling:
|
||||
def test_delete_ignores_notification_cleanup_if_profile_is_gone(self, monkeypatch):
|
||||
target_iccid = "89012804332267989477"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117095243", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_restart_modem", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [Profile("8901240527117095243", "US Mobile", True, "Wireless")],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: object())
|
||||
monkeypatch.setattr(lpa_module, "delete_profile", lambda client, iccid: None)
|
||||
|
||||
def fail_notifications(client):
|
||||
raise RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')
|
||||
|
||||
monkeypatch.setattr(lpa_module, "process_notifications", fail_notifications)
|
||||
|
||||
lpa.delete_profile(target_iccid)
|
||||
|
||||
def test_delete_raises_clear_error_if_profile_still_present_after_cleanup_failure(self, monkeypatch):
|
||||
target_iccid = "89012804332267989477"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117095243", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_restart_modem", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [
|
||||
Profile("8901240527117095243", "US Mobile", True, "Wireless"),
|
||||
Profile(target_iccid, "RedPocket", False, "RedPocket"),
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: object())
|
||||
monkeypatch.setattr(lpa_module, "delete_profile", lambda client, iccid: None)
|
||||
|
||||
def fail_notifications(client):
|
||||
raise RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')
|
||||
|
||||
monkeypatch.setattr(lpa_module, "process_notifications", fail_notifications)
|
||||
|
||||
with pytest.raises(LPAError, match="Profile delete did not finish cleanly"):
|
||||
lpa.delete_profile(target_iccid)
|
||||
|
||||
def test_manager_maps_notification_cleanup_error(self):
|
||||
error = LPAError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')
|
||||
assert EsimManager._map_error(error) == "Modem notification cleanup failed; refresh profiles"
|
||||
|
||||
|
||||
class TestEsimNotificationCleanupRecovery:
|
||||
def test_switch_ignores_notification_cleanup_if_target_is_enabled(self, monkeypatch):
|
||||
target_iccid = "8901240527117194095"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "_ensure_switchable_profile", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117113293", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_wait_for_modem", lambda: None)
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: type("Client", (), {"channel": "2", "_use_csim": False})())
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [
|
||||
Profile("8901240527117113293", "US Mobile", False, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", True, "Wireless"),
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(lpa_module, "enable_profile", lambda client, iccid, refresh=True: None)
|
||||
monkeypatch.setattr(
|
||||
lpa_module,
|
||||
"process_notifications",
|
||||
lambda client: (_ for _ in ()).throw(RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')),
|
||||
)
|
||||
|
||||
lpa.switch_profile(target_iccid)
|
||||
|
||||
@pytest.mark.parametrize(("is_eg25", "expected_refresh", "expected_waits", "expected_reboots"), [
|
||||
(True, True, 1, 0),
|
||||
(False, False, 0, 1),
|
||||
])
|
||||
def test_switch_profile_uses_modem_specific_refresh_behavior(self, monkeypatch, is_eg25, expected_refresh, expected_waits, expected_reboots):
|
||||
target_iccid = "8901240527117194095"
|
||||
lpa = object.__new__(lpa_module.TiciLPA)
|
||||
lpa._is_eg25 = is_eg25
|
||||
lpa.verbose = False
|
||||
|
||||
waits = []
|
||||
reboots = []
|
||||
refresh_values = []
|
||||
|
||||
monkeypatch.setattr(lpa, "_validate_profile_exists", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "_ensure_switchable_profile", lambda iccid: None)
|
||||
monkeypatch.setattr(lpa, "get_active_profile", lambda: Profile("8901240527117113293", "US Mobile", True, "Wireless"))
|
||||
monkeypatch.setattr(lpa, "_wait_for_modem", lambda: waits.append(True))
|
||||
monkeypatch.setattr(lpa, "_restart_modem", lambda: reboots.append(True))
|
||||
monkeypatch.setattr(lpa, "_with_lpa_error", lambda fn: fn())
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: type("Client", (), {"channel": "2", "_use_csim": False})())
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"_process_notifications_after_state_change",
|
||||
lambda validator, _recovery_message, _failure_message: validator(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
lpa,
|
||||
"list_profiles",
|
||||
lambda: [
|
||||
Profile("8901240527117113293", "US Mobile", False, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", True, "Wireless"),
|
||||
],
|
||||
)
|
||||
|
||||
def fake_enable_profile(client, iccid, refresh=True):
|
||||
refresh_values.append(refresh)
|
||||
|
||||
monkeypatch.setattr(lpa_module, "enable_profile", fake_enable_profile)
|
||||
|
||||
lpa.switch_profile(target_iccid)
|
||||
|
||||
assert refresh_values == [expected_refresh]
|
||||
assert len(waits) == expected_waits
|
||||
assert len(reboots) == expected_reboots
|
||||
|
||||
def test_download_ignores_notification_cleanup_if_profile_exists(self, monkeypatch, mocker):
|
||||
target_iccid = "8901240527117194095"
|
||||
lpa = lpa_module.TiciLPA()
|
||||
profiles = [
|
||||
[Profile("8901240527117113293", "US Mobile", True, "Wireless")],
|
||||
[
|
||||
Profile("8901240527117113293", "US Mobile", True, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", False, "Wireless"),
|
||||
],
|
||||
[
|
||||
Profile("8901240527117113293", "US Mobile", True, "Wireless"),
|
||||
Profile(target_iccid, "T-Mobile", False, "Wireless"),
|
||||
],
|
||||
]
|
||||
|
||||
monkeypatch.setattr(lpa, "_ensure_client", lambda: object())
|
||||
monkeypatch.setattr(lpa, "_wait_for_modem", lambda: None)
|
||||
profile_states = iter(profiles)
|
||||
current_profiles = profiles[-1]
|
||||
|
||||
def list_profiles():
|
||||
nonlocal current_profiles
|
||||
current_profiles = next(profile_states, current_profiles)
|
||||
return current_profiles
|
||||
|
||||
monkeypatch.setattr(lpa, "list_profiles", list_profiles)
|
||||
monkeypatch.setattr(lpa_module, "download_profile", lambda client, qr: target_iccid)
|
||||
set_nickname = mocker.MagicMock()
|
||||
monkeypatch.setattr(lpa_module, "set_profile_nickname", set_nickname)
|
||||
monkeypatch.setattr(
|
||||
lpa_module,
|
||||
"process_notifications",
|
||||
lambda client: (_ for _ in ()).throw(RuntimeError('AT command failed (AT+CGLA=2,16,"80E2910003BF2800"): AT command failed')),
|
||||
)
|
||||
|
||||
lpa.download_profile(TEST_ACTIVATION_CODE, "T-Mobile")
|
||||
|
||||
set_nickname.assert_called_once_with(mocker.ANY, target_iccid, "T-Mobile")
|
||||
|
||||
|
||||
class TestEsimManagerSupportGating:
|
||||
def test_refresh_profiles_does_not_touch_lpa_without_euicc(self, monkeypatch, mocker):
|
||||
manager = EsimManager()
|
||||
|
||||
monkeypatch.setattr(manager, "_query_euicc_support", lambda: False)
|
||||
manager._params = mocker.MagicMock()
|
||||
manager._params.get.return_value = None
|
||||
manager._params.get_bool.return_value = True
|
||||
monkeypatch.setattr(HARDWARE, "get_device_type", lambda: "tici")
|
||||
monkeypatch.setattr(HARDWARE, "get_sim_lpa", lambda: (_ for _ in ()).throw(AssertionError("LPA should not be touched")))
|
||||
|
||||
manager.refresh_profiles()
|
||||
|
||||
assert manager.get_state().profiles == []
|
||||
assert manager.get_state().message == "Insert the original comma SIM card that came with the device to use eSIM"
|
||||
102
iqpilot/system/hardware/tici/tests/test_hardware.py
Normal file
102
iqpilot/system/hardware/tici/tests/test_hardware.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from iqpilot.system.hardware.tici.hardware import (
|
||||
MM_MODEM_ACCESS_TECHNOLOGY_LTE,
|
||||
MM_MODEM_STATE,
|
||||
NMActiveConnectionState,
|
||||
Tici,
|
||||
)
|
||||
from iqpilot.cereal import log
|
||||
|
||||
|
||||
def _make_connection(mocker, connection_type: str, state: int):
|
||||
connection = mocker.MagicMock()
|
||||
|
||||
def get_side_effect(_iface, prop, **_kwargs):
|
||||
values = {
|
||||
"Type": connection_type,
|
||||
"State": state,
|
||||
}
|
||||
return values[prop]
|
||||
|
||||
connection.Get.side_effect = get_side_effect
|
||||
return connection
|
||||
|
||||
|
||||
def test_reboot_modem_falls_back_to_direct_at(monkeypatch, mocker):
|
||||
device = Tici()
|
||||
direct_runner = mocker.MagicMock()
|
||||
|
||||
monkeypatch.setattr(device, "get_modem", mocker.MagicMock(side_effect=ModuleNotFoundError("dbus")))
|
||||
monkeypatch.setattr(device, "_run_direct_modem_command", direct_runner)
|
||||
|
||||
device.reboot_modem()
|
||||
|
||||
assert direct_runner.call_args_list == [
|
||||
(("AT+CFUN=0",), {}),
|
||||
(("AT+CFUN=1",), {}),
|
||||
]
|
||||
|
||||
|
||||
def test_get_network_type_ignores_non_activated_cellular(mocker):
|
||||
device = Tici()
|
||||
primary = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATING)
|
||||
bus = mocker.MagicMock()
|
||||
bus.get_object.return_value = primary
|
||||
nm = mocker.MagicMock()
|
||||
nm.Get.return_value = "/primary"
|
||||
|
||||
device.__dict__["bus"] = bus
|
||||
device.__dict__["nm"] = nm
|
||||
|
||||
assert device.get_network_type() == log.DeviceState.NetworkType.none
|
||||
|
||||
|
||||
def test_get_network_type_requires_registered_modem(monkeypatch, mocker):
|
||||
device = Tici()
|
||||
primary = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
cellular = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
bus = mocker.MagicMock()
|
||||
bus.get_object.side_effect = [primary, cellular]
|
||||
nm = mocker.MagicMock()
|
||||
nm.Get.side_effect = ["/primary", ["/cellular"]]
|
||||
modem = mocker.MagicMock()
|
||||
|
||||
def modem_get_side_effect(_iface, prop, **_kwargs):
|
||||
values = {
|
||||
"State": MM_MODEM_STATE.SEARCHING,
|
||||
"AccessTechnologies": MM_MODEM_ACCESS_TECHNOLOGY_LTE,
|
||||
}
|
||||
return values[prop]
|
||||
|
||||
modem.Get.side_effect = modem_get_side_effect
|
||||
|
||||
device.__dict__["bus"] = bus
|
||||
device.__dict__["nm"] = nm
|
||||
monkeypatch.setattr(device, "get_modem", mocker.MagicMock(return_value=modem))
|
||||
|
||||
assert device.get_network_type() == log.DeviceState.NetworkType.none
|
||||
|
||||
|
||||
def test_get_network_type_reports_lte_for_registered_modem(monkeypatch, mocker):
|
||||
device = Tici()
|
||||
primary = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
cellular = _make_connection(mocker, "gsm", NMActiveConnectionState.ACTIVATED)
|
||||
bus = mocker.MagicMock()
|
||||
bus.get_object.side_effect = [primary, cellular]
|
||||
nm = mocker.MagicMock()
|
||||
nm.Get.side_effect = ["/primary", ["/cellular"]]
|
||||
modem = mocker.MagicMock()
|
||||
|
||||
def modem_get_side_effect(_iface, prop, **_kwargs):
|
||||
values = {
|
||||
"State": MM_MODEM_STATE.CONNECTED,
|
||||
"AccessTechnologies": MM_MODEM_ACCESS_TECHNOLOGY_LTE,
|
||||
}
|
||||
return values[prop]
|
||||
|
||||
modem.Get.side_effect = modem_get_side_effect
|
||||
|
||||
device.__dict__["bus"] = bus
|
||||
device.__dict__["nm"] = nm
|
||||
monkeypatch.setattr(device, "get_modem", mocker.MagicMock(return_value=modem))
|
||||
|
||||
assert device.get_network_type() == log.DeviceState.NetworkType.cell4G
|
||||
53
iqpilot/system/hardware/tici/tests/test_lpa_activation.py
Normal file
53
iqpilot/system/hardware/tici/tests/test_lpa_activation.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
from iqpilot.system.hardware.tici import qr_decode as qr_decode_module
|
||||
from iqpilot.system.hardware.tici.lpa import parse_lpa_activation_code
|
||||
from iqpilot.system.hardware.tici.qr_decode import validate_lpa_activation_code
|
||||
|
||||
|
||||
def test_parse_valid_activation_code():
|
||||
version, smdp, matching = parse_lpa_activation_code("LPA:1$rsp.truphone.com$QRF-BETTERROAMING")
|
||||
assert version == "1"
|
||||
assert smdp == "rsp.truphone.com"
|
||||
assert matching == "QRF-BETTERROAMING"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code", [
|
||||
"",
|
||||
"foo",
|
||||
"LPA:2$rsp.truphone.com$abc",
|
||||
"LPA:1$$abc",
|
||||
"LPA:1$rsp.truphone.com$",
|
||||
"LPA:1$rsp.truphone.com",
|
||||
])
|
||||
def test_parse_invalid_activation_code(code):
|
||||
with pytest.raises(ValueError):
|
||||
parse_lpa_activation_code(code)
|
||||
|
||||
|
||||
def test_qr_validator_valid():
|
||||
valid, reason = validate_lpa_activation_code("LPA:1$rsp.truphone.com$QRF-123")
|
||||
assert valid
|
||||
assert reason == ""
|
||||
|
||||
|
||||
def test_qr_validator_invalid():
|
||||
valid, reason = validate_lpa_activation_code("https://example.com")
|
||||
assert not valid
|
||||
assert reason
|
||||
|
||||
|
||||
def test_decode_qr_prefers_pyzbar(monkeypatch):
|
||||
class FakeResult:
|
||||
data = b"LPA:1$rsp.truphone.com$QRF-123"
|
||||
|
||||
monkeypatch.setattr(qr_decode_module, "_pyzbar_decode", lambda arr: [FakeResult()])
|
||||
|
||||
def fail_load_decoder():
|
||||
raise AssertionError("quirc fallback should not be used when pyzbar succeeds")
|
||||
|
||||
monkeypatch.setattr(qr_decode_module, "_load_decoder", fail_load_decoder)
|
||||
|
||||
payloads = qr_decode_module.decode_qr(np.zeros((4, 4), dtype=np.uint8))
|
||||
assert payloads == ["LPA:1$rsp.truphone.com$QRF-123"]
|
||||
128
iqpilot/system/hardware/tici/tests/test_power_draw.py
Normal file
128
iqpilot/system/hardware/tici/tests/test_power_draw.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from collections import defaultdict, deque
|
||||
import pytest
|
||||
import time
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from tabulate import tabulate
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.cereal.services import SERVICE_LIST
|
||||
from iqdbc.car.car_helpers import get_demo_car_params
|
||||
from iqpilot.common.mock import mock_messages
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.hardware.tici.power_monitor import get_power
|
||||
from iqpilot.system.manager.process_config import managed_processes
|
||||
from iqpilot.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'], 1.65, atol=0.4, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
|
||||
Proc(['modeld'], 1.24, atol=0.2, msgs=['modelV2']),
|
||||
Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']),
|
||||
Proc(['encoderd'], 0.23, msgs=[]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPowerDraw:
|
||||
|
||||
def setup_method(self):
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes())
|
||||
|
||||
# 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.isclose(msgs_expected, msgs_received, rtol=.02, atol=2)
|
||||
|
||||
def valid_power_draw(self, proc, used):
|
||||
return np.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(['deviceMotion'])
|
||||
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")
|
||||
Reference in New Issue
Block a user